refactor: remove todo tool (#35989)

This commit is contained in:
Aiden Cline 2026-07-09 00:13:48 -05:00 committed by GitHub
parent d4155f2906
commit 7feefb697f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
250 changed files with 237 additions and 4755 deletions

View file

@ -116,7 +116,6 @@ it.instance("explore agent denies edit and write", () =>
expect(explore?.mode).toBe("subagent")
expect(evalPerm(explore, "edit")).toBe("deny")
expect(evalPerm(explore, "write")).toBe("deny")
expect(evalPerm(explore, "todowrite")).toBe("deny")
}),
)
@ -160,16 +159,6 @@ it.instance(
},
)
it.instance("general agent denies todo tools", () =>
Effect.gen(function* () {
const general = yield* load((svc) => svc.get("general"))
expect(general).toBeDefined()
expect(general?.mode).toBe("subagent")
expect(general?.hidden).toBeUndefined()
expect(evalPerm(general, "todowrite")).toBe("deny")
}),
)
it.instance("compaction agent denies all permissions", () =>
Effect.gen(function* () {
const compaction = yield* load((svc) => svc.get("compaction"))

View file

@ -93,7 +93,7 @@ describe("extractResponseText", () => {
})
test("returns text even when tool parts follow", () => {
const parts = [createTextPart("I'll help with that."), createToolPart("todowrite", "3 todos")]
const parts = [createTextPart("I'll help with that."), createToolPart("read", "src/index.ts")]
expect(extractResponseText(parts)).toBe("I'll help with that.")
})
@ -103,8 +103,7 @@ describe("extractResponseText", () => {
})
test("returns null for tool-only response (signals summary needed)", () => {
// This is the exact scenario from the bug report - todowrite with no text
const parts = [createToolPart("todowrite", "8 todos")]
const parts = [createToolPart("read", "src/index.ts")]
expect(extractResponseText(parts)).toBeNull()
})

View file

@ -519,8 +519,8 @@ Options:
--description what the agent should do [string]
--mode agent mode [string] [choices: "all", "primary", "subagent"]
--permissions, --tools comma-separated list of permissions to allow (default: all).
Available: "bash, read, edit, glob, grep, webfetch, task, todowrite,
websearch, lsp, skill" [string]
Available: "bash, read, edit, glob, grep, webfetch, task, websearch,
lsp, skill" [string]
-m, --model model to use in the format of provider/model [string]"
`;

View file

@ -321,50 +321,8 @@ test("holds markdown code blocks until final commit and keeps newline ownership"
}
})
test("renders todo and question summaries without boilerplate footer copy", async () => {
test("renders question summaries without boilerplate footer copy", async () => {
const cases = [
{
title: "# Todos",
include: [
"[✓] List files under `run/`",
"[•] Count functions in each `run/` file",
"[ ] Mark each tracking item complete",
],
exclude: ["Updating", "todos completed"],
start: toolCommit({
tool: "todowrite",
phase: "start",
toolState: "running",
state: {
status: "running",
input: {
todos: [
{ status: "completed", content: "List files under `run/`" },
{ status: "in_progress", content: "Count functions in each `run/` file" },
{ status: "pending", content: "Mark each tracking item complete" },
],
},
time: { start: 1 },
},
}),
final: toolCommit({
tool: "todowrite",
phase: "final",
toolState: "completed",
state: {
status: "completed",
input: {
todos: [
{ status: "completed", content: "List files under `run/`" },
{ status: "in_progress", content: "Count functions in each `run/` file" },
{ status: "pending", content: "Mark each tracking item complete" },
],
},
metadata: {},
time: { start: 1, end: 4 },
},
}),
},
{
title: "# Questions",
include: ["What should I work on in the codebase next?", "Bug fix"],

View file

@ -220,7 +220,6 @@ export default {
api.kv.set(options.kv_key, "stored")
const kv_after = api.kv.get(options.kv_key, "missing")
const diff = api.state.session.diff(options.session_id)
const todo = api.state.session.todo(options.session_id)
const lsp = api.state.lsp()
const mcp = api.state.mcp()
const depth_before = api.ui.dialog.depth
@ -263,8 +262,6 @@ export default {
kv_ready: api.kv.ready,
diff_count: diff.length,
diff_file: diff[0]?.file,
todo_count: todo.length,
todo_first: todo[0]?.content,
lsp_count: lsp.length,
mcp_count: mcp.length,
mcp_first: mcp[0]?.name,
@ -513,10 +510,6 @@ export default {
if (sessionID !== "ses_test") return []
return [{ file: "src/app.ts", additions: 3, deletions: 1 }]
},
todo(sessionID) {
if (sessionID !== "ses_test") return []
return [{ content: "ship it", status: "pending" }]
},
},
lsp() {
return [{ id: "ts", root: "/tmp/project", status: "connected" }]
@ -867,8 +860,6 @@ describe("tui.plugin.loader", () => {
expect(data.local.kv_ready).toBe(true)
expect(data.local.diff_count).toBe(1)
expect(data.local.diff_file).toBe("src/app.ts")
expect(data.local.todo_count).toBe(1)
expect(data.local.todo_first).toBe("ship it")
expect(data.local.lsp_count).toBe(1)
expect(data.local.mcp_count).toBe(1)
expect(data.local.mcp_first).toBe("github")

View file

@ -1307,7 +1307,6 @@ it.instance("permission config preserves user key order", () =>
write: "ask",
external_directory: "ask",
read: "allow",
todowrite: "allow",
"thoughts_*": "allow",
"reasoning_model_*": "allow",
"tools_*": "allow",
@ -1322,7 +1321,6 @@ it.instance("permission config preserves user key order", () =>
"write",
"external_directory",
"read",
"todowrite",
"thoughts_*",
"reasoning_model_*",
"tools_*",

View file

@ -2,7 +2,6 @@ import { describe, expect, test } from "bun:test"
import { SessionEvent } from "@opencode-ai/core/session/event"
import { Agent } from "@opencode-ai/schema"
import { EventManifest as SchemaEventManifest } from "@opencode-ai/schema/event-manifest"
import { Todo } from "@/session/todo"
import { EventManifest } from "@/event-manifest"
describe("public event manifest", () => {
@ -10,10 +9,9 @@ describe("public event manifest", () => {
expect(EventManifest.Definitions).toBe(SchemaEventManifest.Definitions)
expect(EventManifest.Latest).toBe(SchemaEventManifest.Latest)
expect(EventManifest.Durable).toBe(SchemaEventManifest.Durable)
expect(Array.from(EventManifest.Latest.keys()).filter((type) => type !== "agent.updated").length).toBe(101)
expect(Array.from(EventManifest.Latest.keys()).filter((type) => type !== "agent.updated").length).toBe(108)
expect(EventManifest.Latest.get("session.next.step.ended")).toBe(SessionEvent.Step.Ended)
expect(EventManifest.Latest.get("agent.updated")).toBe(Agent.Event.Updated)
expect(EventManifest.Latest.get("todo.updated")).toBe(Todo.Event.Updated)
expect(EventManifest.Latest.has("ide.installed")).toBe(false)
expect(EventManifest.Latest.has("server.connected")).toBe(true)
expect(EventManifest.Latest.has("global.disposed")).toBe(true)

View file

@ -316,7 +316,6 @@ export function createTuiPluginApi(opts: Opts = {}): HostPluginApi {
count: opts.state?.session?.count ?? (() => 0),
get: opts.state?.session?.get ?? (() => undefined),
diff: opts.state?.session?.diff ?? (() => []),
todo: opts.state?.session?.todo ?? (() => []),
messages: opts.state?.session?.messages ?? (() => []),
status: opts.state?.session?.status ?? (() => undefined),
permission: opts.state?.session?.permission ?? (() => []),

View file

@ -1326,23 +1326,6 @@ const scenarios: Scenario[] = [
"children should include seeded child",
)
}),
http.protected
.get("/session/{sessionID}/todo", "session.todo")
.seeded((ctx) =>
Effect.gen(function* () {
const session = yield* ctx.session({ title: "Todo session" })
const todos = [{ content: "cover session todo", status: "pending" as const, priority: "high" as const }]
yield* ctx.todos(session.id, todos)
return { session, todos }
}),
)
.at((ctx) => ({
path: route("/session/{sessionID}/todo", { sessionID: ctx.state.session.id }),
headers: ctx.headers(),
}))
.json(200, (body, ctx) => {
check(stable(body) === stable(ctx.state.todos), "todos should match seeded state")
}),
http.protected
.get("/session/{sessionID}/diff", "session.diff")
.seeded((ctx) => ctx.session({ title: "Diff session" }))

View file

@ -176,7 +176,6 @@ function withContext<A, E>(
}),
messages: (sessionID) =>
run(modules.Session.Service.use((svc) => svc.messages({ sessionID }).pipe(Effect.orDie))),
todos: (sessionID, todos) => run(modules.Todo.Service.use((svc) => svc.update({ sessionID, todos }))),
worktree: (input) => run(modules.Worktree.Service.use((svc) => svc.create(input).pipe(Effect.orDie))),
worktreeRemove: (directory) =>
run(modules.Worktree.Service.use((svc) => svc.remove({ directory })).pipe(Effect.ignore)),

View file

@ -6,7 +6,6 @@ export type Runtime = {
InstanceRef: (typeof import("../../../src/effect/instance-ref"))["InstanceRef"]
InstanceStore: (typeof import("../../../src/project/instance-store"))["InstanceStore"]
Session: (typeof import("../../../src/session/session"))["Session"]
Todo: (typeof import("../../../src/session/todo"))["Todo"]
Worktree: (typeof import("../../../src/worktree"))["Worktree"]
Project: (typeof import("../../../src/project/project"))["Project"]
Tui: typeof import("../../../src/server/shared/tui-control")
@ -26,7 +25,6 @@ export function runtime() {
const instanceRef = await import("../../../src/effect/instance-ref")
const instanceStore = await import("../../../src/project/instance-store")
const session = await import("../../../src/session/session")
const todo = await import("../../../src/session/todo")
const worktree = await import("../../../src/worktree")
const project = await import("../../../src/project/project")
const tui = await import("../../../src/server/shared/tui-control")
@ -40,7 +38,6 @@ export function runtime() {
InstanceRef: instanceRef.InstanceRef,
InstanceStore: instanceStore.InstanceStore,
Session: session.Session,
Todo: todo.Todo,
Worktree: worktree.Worktree,
Project: project.Project,
Tui: tui,

View file

@ -60,7 +60,6 @@ export type ScenarioContext = {
project: () => Effect.Effect<Project.Info>
message: (sessionID: SessionID, input?: { text?: string }) => Effect.Effect<MessageSeed>
messages: (sessionID: SessionID) => Effect.Effect<SessionV1.WithParts[]>
todos: (sessionID: SessionID, todos: TodoInfo[]) => Effect.Effect<void>
worktree: (input?: { name?: string }) => Effect.Effect<Worktree.Info>
worktreeRemove: (directory: string) => Effect.Effect<void>
llmText: (value: string) => Effect.Effect<void>
@ -119,9 +118,4 @@ export type Result =
| { status: "skip"; scenario: TodoScenario }
export type SessionInfo = { id: SessionID; title: string; parentID?: SessionID }
export type TodoInfo = {
content: string
status: "pending" | "in_progress" | "completed" | "cancelled"
priority: "high" | "medium" | "low"
}
export type MessageSeed = { info: SessionV1.User; part: SessionV1.TextPart }

View file

@ -577,7 +577,6 @@ describe("HttpApi SDK", () => {
const roots = yield* capture(() => sdk.session.list({ roots: true, limit: 10 }))
const all = yield* capture(() => sdk.session.list({ roots: false, limit: 10 }))
const children = yield* capture(() => sdk.session.children({ sessionID: parentID }))
const todo = yield* capture(() => sdk.session.todo({ sessionID: parentID }))
const status = yield* capture(() => sdk.session.status())
const messages = yield* capture(() => sdk.session.messages({ sessionID: parentID }))
const missingGet = yield* capture(() => sdk.session.get({ sessionID: "ses_missing" }))
@ -597,7 +596,6 @@ describe("HttpApi SDK", () => {
roots,
all,
children,
todo,
status,
messages,
missingGet,
@ -611,7 +609,6 @@ describe("HttpApi SDK", () => {
rootTitles: sessionTitles(roots.data),
allTitles: sessionTitles(all.data),
childCount: array(children.data).length,
todoCount: array(todo.data).length,
messageCount: array(messages.data).length,
}
}),

View file

@ -272,10 +272,6 @@ describe("session HttpApi", () => {
expect(children.status).toBe(404)
expect(yield* responseJson(children)).toEqual(missingSessionBody)
const todo = yield* request(pathFor(SessionPaths.todo, { sessionID: missingSession }), { headers })
expect(todo.status).toBe(404)
expect(yield* responseJson(todo)).toEqual(missingSessionBody)
const messages = yield* request(pathFor(SessionPaths.messages, { sessionID: missingSession }), { headers })
expect(messages.status).toBe(404)
expect(yield* responseJson(messages)).toEqual(missingSessionBody)
@ -344,10 +340,6 @@ describe("session HttpApi", () => {
})).map((item) => item.id),
).toEqual([child.id])
expect(
yield* requestJson<unknown[]>(pathFor(SessionPaths.todo, { sessionID: parent.id }), { headers }),
).toEqual([])
expect(
yield* requestJson<unknown[]>(pathFor(SessionPaths.diff, { sessionID: parent.id }), { headers }),
).toEqual([])

View file

@ -24,7 +24,6 @@ import { Git } from "../../src/git"
import { Image } from "../../src/image/image"
import { Question } from "../../src/question"
import { Todo } from "../../src/session/todo"
import { Session } from "@/session/session"
import { SessionMessageTable } from "@opencode-ai/core/session/sql"
import { LLM } from "../../src/session/llm"
@ -191,7 +190,6 @@ const promptRoot = LayerNode.group([
Database.node,
EventV2Bridge.node,
Question.node,
Todo.node,
ToolRegistry.node,
Skill.node,
Git.node,

View file

@ -6,7 +6,6 @@ import { SessionPrompt } from "../../src/session/prompt"
import { SessionRevert } from "../../src/session/revert"
import { SessionStatus } from "../../src/session/status"
import { SessionSummary } from "../../src/session/summary"
import { Todo } from "../../src/session/todo"
import { SessionID, MessageID, PartID } from "../../src/session/schema"
import { ProjectV2 } from "@opencode-ai/core/project"
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
@ -252,15 +251,6 @@ describe("SessionStatus.Info", () => {
})
})
describe("Todo.Info", () => {
const decode = decodeUnknown(Todo.Info)
test("three-field round-trip", () => {
const input = Todo.Info.make({ content: "do a thing", status: "pending", priority: "high" })
expect(decode(input)).toEqual(input)
})
})
describe("SessionPrompt input schemas", () => {
test("LoopInput is just sessionID", () => {
const decode = decodeUnknown(SessionPrompt.LoopInput)

View file

@ -335,44 +335,6 @@ exports[`tool parameters JSON Schema (wire shape) task 1`] = `
}
`;
exports[`tool parameters JSON Schema (wire shape) todo 1`] = `
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"todos": {
"description": "The updated todo list",
"items": {
"properties": {
"content": {
"description": "Brief description of the task",
"type": "string",
},
"priority": {
"description": "Priority level of the task: high, medium, low",
"type": "string",
},
"status": {
"description": "Current status of the task: pending, in_progress, completed, cancelled",
"type": "string",
},
},
"required": [
"content",
"status",
"priority",
],
"type": "object",
},
"type": "array",
},
},
"required": [
"todos",
],
"type": "object",
}
`;
exports[`tool parameters JSON Schema (wire shape) webfetch 1`] = `
{
"$schema": "https://json-schema.org/draft/2020-12/schema",

View file

@ -21,7 +21,6 @@ import { Parameters as Read } from "../../src/tool/read"
import { Parameters as Shell } from "../../src/tool/shell"
import { Parameters as Skill } from "../../src/tool/skill"
import { Parameters as Task } from "../../src/tool/task"
import { Parameters as Todo } from "../../src/tool/todo"
import { Parameters as WebFetch } from "../../src/tool/webfetch"
import { Parameters as WebSearch } from "../../src/tool/websearch"
import { Parameters as Write } from "../../src/tool/write"
@ -48,7 +47,6 @@ describe("tool parameters", () => {
test("read", () => expect(toJsonSchema(Read)).toMatchSnapshot())
test("skill", () => expect(toJsonSchema(Skill)).toMatchSnapshot())
test("task", () => expect(toJsonSchema(Task)).toMatchSnapshot())
test("todo", () => expect(toJsonSchema(Todo)).toMatchSnapshot())
test("webfetch", () => expect(toJsonSchema(WebFetch)).toMatchSnapshot())
test("websearch", () => expect(toJsonSchema(WebSearch)).toMatchSnapshot())
test("write", () => expect(toJsonSchema(Write)).toMatchSnapshot())
@ -248,18 +246,6 @@ describe("tool parameters", () => {
})
})
describe("todo", () => {
test("accepts todos array", () => {
const parsed = parse(Todo, {
todos: [{ id: "t1", content: "do x", status: "pending", priority: "medium" }],
})
expect(parsed.todos.length).toBe(1)
})
test("rejects missing todos", () => {
expect(accepts(Todo, {})).toBe(false)
})
})
describe("webfetch", () => {
test("defaults omitted format to markdown", () => {
expect(parse(WebFetch, { url: "https://example.com" })).toEqual({

View file

@ -388,7 +388,7 @@ describe("tool.task", () => {
)
it.instance(
"execute shapes child permissions for task, todowrite, and primary tools",
"execute shapes child permissions for task and primary tools",
() =>
Effect.gen(function* () {
const sessions = yield* Session.Service
@ -420,11 +420,6 @@ describe("tool.task", () => {
expect(child.parentID).toBe(chat.id)
expect(child.agent).toBe("reviewer")
expect(child.permission).toEqual([
{
permission: "todowrite",
pattern: "*",
action: "deny",
},
{
permission: "bash",
pattern: "*",