Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ test.describe("timeline tool state stability", () => {
const initial = [
...ids.map((id) => toolPart(`prt_state_${id}`, names[id], "streaming", inputs[id])),
toolPart(questionID, "question", "streaming", questionInput()),
toolPart(todoID, "todowrite", "streaming", { todos: [{ content: "Hidden", status: "pending" }] }),
toolPart(todoID, "todo", "streaming", { todos: [{ content: "Hidden", status: "pending" }] }),
textPart("prt_state_following", "Following lightweight tools"),
]

Expand Down
16 changes: 7 additions & 9 deletions packages/core/src/database/v1-migration.bun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -900,7 +900,8 @@ function row(

const RENAMED_TOOLS: Readonly<Record<string, string>> = { bash: "shell", task: "subagent", apply_patch: "patch" }
const PATH_TOOLS = ["read", "edit", "write"]
const REMOVED_TOOLS = ["todowrite"]
// Replacements that are only registered when an experimental flag enables them.
const REPLACED_TOOLS: Readonly<Record<string, string>> = { todowrite: "todo" }

/**
* Tells the model about V1 tools it called in the still-visible history whose
Expand All @@ -920,7 +921,7 @@ function legacyToolNotice(messages: ReadonlyArray<TransformResult["messages"][nu
const list = (names: ReadonlyArray<string>) => names.map((name) => `\`${name}\``).join(", ")
const renamed = Object.keys(RENAMED_TOOLS).filter((name) => called.has(name))
const paths = PATH_TOOLS.filter((name) => called.has(name))
const removed = REMOVED_TOOLS.filter((name) => called.has(name))
const replaced = Object.keys(REPLACED_TOOLS).filter((name) => called.has(name))
const parts = [
...(renamed.length === 1
? [`The \`${renamed[0]}\` tool is now \`${RENAMED_TOOLS[renamed[0]]}\` and must be called by that name.`]
Expand All @@ -940,13 +941,10 @@ function legacyToolNotice(messages: ReadonlyArray<TransformResult["messages"][nu
? [`The following tools now take \`path\` instead of \`filePath\`: ${list(paths)}.`]
: []),
...(called.has("skill") ? ["The `skill` tool now takes `id` instead of `name`."] : []),
...(removed.length === 1
? [`The \`${removed[0]}\` tool is no longer available by default. Only call it if it is in your tool list.`]
: removed.length > 1
? [
`The following tools are no longer available by default. Only call them if they are in your tool list: ${list(removed)}.`,
]
: []),
...replaced.map(
(name) =>
`The \`${name}\` tool is no longer available. If \`${REPLACED_TOOLS[name]}\` is in your tool list, use it instead.`,
),
]
if (parts.length === 0) return undefined
return ["The available tools have changed.", ...parts].join("\n\n")
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/tool/plugin/todo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { Effect, Schema } from "effect"
import { Config } from "../../config.js"
import { ConfigEntryObserver } from "../../config/plugin/entry-observer.js"
import { Permission } from "../../permission.js"
import DESCRIPTION from "../todowrite.txt"
import DESCRIPTION from "../todo.txt"

export const name = "todowrite"
export const name = "todo"

export const Info = Schema.Struct({
content: Schema.String.annotate({ description: "Brief description of the task" }),
Expand Down
File renamed without changes.
1 change: 1 addition & 0 deletions packages/core/src/v1/config/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function normalizeAction(action: string) {
if (action === "write" || action === "patch") return "edit"
if (action === "task") return "subagent"
if (action === "bash") return "shell"
if (action === "todowrite") return "todo"
return action
}

Expand Down
2 changes: 1 addition & 1 deletion packages/core/test/tool-todo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe("TodoTool", () => {
}),
)

it.effect("registers todowrite on the native tool list and summarizes the update", () =>
it.effect("registers todo on the native tool list and summarizes the update", () =>
Effect.gen(function* () {
const registry = yield* setup(enabled)
const definition = (yield* toolDefinitions(registry)).find((tool) => tool.name === TodoTool.name)
Expand Down
4 changes: 2 additions & 2 deletions packages/core/test/v1-migration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ describe("V1Migration.transformSession", () => {
})
})

test("appends a legacy tool notice naming only the renamed or removed tools that were called", () => {
test("appends a legacy tool notice naming only the renamed or replaced tools that were called", () => {
const parent = user("msg_000000000060aaaaaaaaaaaaaa")
const message = assistant("msg_000000000061aaaaaaaaaaaaaa", parent.id)
const call = (id: string, tool: string) =>
Expand Down Expand Up @@ -505,7 +505,7 @@ describe("V1Migration.transformSession", () => {
"The following tools were renamed and must be called by their new names: `bash` is now `shell`; `task` is now `subagent`; `apply_patch` is now `patch`.",
"The `subagent` tool takes `agent` instead of `subagent_type` and `sessionID` instead of `task_id`.",
"The `read` tool now takes `path` instead of `filePath`.",
"The `todowrite` tool is no longer available by default. Only call it if it is in your tool list.",
"The `todowrite` tool is no longer available. If `todo` is in your tool list, use it instead.",
].join("\n\n"),
time: { created: 20 },
},
Expand Down
2 changes: 1 addition & 1 deletion packages/schema/src/config/experimental.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export class Info extends Schema.Class<Info>("ConfigExperimental.Info")({
description: "Enable the experimental portable shell permission scanner. Defaults to false.",
}),
todo: Schema.Boolean.pipe(optional).annotate({
description: "Enable the experimental todowrite tool. Defaults to false.",
description: "Enable the experimental todo tool. Defaults to false.",
}),
subagent_depth: NonNegativeInt.pipe(optional).annotate({
description: "Maximum subagent nesting depth. Defaults to 1.",
Expand Down
2 changes: 1 addition & 1 deletion packages/session-ui/src/timeline/projection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -840,7 +840,7 @@ function renderable(content: Content, showReasoning: boolean, detail?: TimelineD

if (detail && currentToolFailed(content)) return true

if (content.name === "todowrite") return false
if (content.name === "todo" || content.name === "todowrite") return false

if (content.name === "question") return content.state.status !== "streaming" && content.state.status !== "running"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ const CompleteAgentWorkflow = {
metadata: { files: [storyPatchFile("src/a.ts")] },
},
),
storyTool("tool_family_todo", "todowrite", "completed", { todos: [{ content: "Hidden", status: "pending" }] }),
storyTool("tool_family_todo", "todo", "completed", { todos: [{ content: "Hidden", status: "pending" }] }),
storyTool("tool_family_question", "question", "completed", questions, { metadata: { answers: [["Yes"]] } }),
storyTool("tool_family_skill", "skill", "completed", { name: "stability" }),
storyTool("tool_family_custom", "custom_mcp_tool", "completed", { target: "timeline" }),
Expand Down Expand Up @@ -311,7 +311,7 @@ const RecoverFromToolFailures = {
storyTool("tool_error_question_transport", "question", "error", questions, {
error: "Question transport failed",
}),
storyTool("tool_error_todo", "todowrite", "error", { todos: [] }, { error: "Hidden todo failure" }),
storyTool("tool_error_todo", "todo", "error", { todos: [] }, { error: "Hidden todo failure" }),
])}
width="860px"
/>
Expand Down
7 changes: 7 additions & 0 deletions packages/session-ui/src/timeline/rows-current.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,13 @@ describe("current session timeline rows", () => {
{
type: "tool",
id: "tool_todo",
name: "todo",
state: { status: "running", input: {}, metadata: {} },
time: { created: 4 },
},
{
type: "tool",
id: "tool_todo_legacy",
name: "todowrite",
state: { status: "running", input: {}, metadata: {} },
time: { created: 4 },
Expand Down
2 changes: 1 addition & 1 deletion packages/session-ui/src/tools/tool-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1349,7 +1349,7 @@ export function ToolDisplay(
const data = useData()
const i18n = useI18n()

if (props.tool === "todowrite") return null
if (props.tool === "todo" || props.tool === "todowrite") return null
const hideQuestion = () => props.tool === "question" && (props.status === "streaming" || props.status === "running")

const taskId = createMemo(() => {
Expand Down
10 changes: 5 additions & 5 deletions packages/tui/src/feature-plugins/system/storybook/todo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ const statuses: TodoStatus[] = ["pending", "in_progress", "completed", "cancelle

const initial = [
{ content: "Read the existing sidebar plugins", status: "completed" as TodoStatus },
{ content: "Derive the current list from the latest todowrite call", status: "in_progress" as TodoStatus },
{ content: "Render the todowrite tool part like V1", status: "pending" as TodoStatus },
{ content: "Derive the current list from the latest todo call", status: "in_progress" as TodoStatus },
{ content: "Render the todo tool part like V1", status: "pending" as TodoStatus },
{ content: "Add a database table for todos", status: "cancelled" as TodoStatus },
]

Expand All @@ -31,8 +31,8 @@ function TodoStory(props: { context: Plugin.Context }) {
content: [
{
type: "tool",
id: "fixture-todowrite",
name: "todowrite",
id: "fixture-todo",
name: "todo",
time: { created: 0, completed: 1 },
state: {
status: "completed",
Expand Down Expand Up @@ -125,7 +125,7 @@ function TodoStory(props: { context: Plugin.Context }) {
<box width={dimensions().width} height={dimensions().height} backgroundColor={theme.background.base}>
<box flexDirection="row" flexGrow={1} paddingTop={1} paddingLeft={2} paddingRight={2} gap={4}>
<box flexGrow={1} flexDirection="column">
<text fg={theme.text.base}>Fixture todowrite input</text>
<text fg={theme.text.base}>Fixture todo input</text>
<box height={1} />
<For each={todos()}>
{(todo, index) => (
Expand Down
2 changes: 1 addition & 1 deletion packages/tui/src/mini/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,7 @@ function emitTodo(state: State): void {
{ content: "Tune tool formatting", status: "pending" },
{ content: "Rewrite the renderer", status: "cancelled" },
]
const ref = make(state, "todowrite", { todos })
const ref = make(state, "todo", { todos })
doneTool(state, ref, { output: "1 completed, 1 in progress, 1 pending, 1 cancelled", metadata: { todos } })
}

Expand Down
4 changes: 2 additions & 2 deletions packages/tui/src/mini/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ type ToolName =
| "patch"
| "batch"
| "subagent"
| "todowrite"
| "todo"
| "question"
| "read"
| "glob"
Expand Down Expand Up @@ -994,7 +994,7 @@ const TOOL_RULES = {
final: scrollTaskFinal,
},
},
todowrite: {
todo: {
view: {
output: false,
final: true,
Expand Down
6 changes: 3 additions & 3 deletions packages/tui/src/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2472,8 +2472,8 @@ function ToolPart(props: { part: SessionMessageAssistantTool; images?: boolean }
<Match when={display() === "patch"}>
<ApplyPatch {...toolprops} />
</Match>
<Match when={display() === "todowrite"}>
<TodoWrite {...toolprops} />
<Match when={display() === "todo"}>
<Todo {...toolprops} />
</Match>
<Match when={display() === "question"}>
<Question {...toolprops} />
Expand Down Expand Up @@ -3471,7 +3471,7 @@ function ApplyPatch(props: ToolProps) {
)
}

function TodoWrite(props: ToolProps) {
function Todo(props: ToolProps) {
const theme = useTheme()
const todos = createMemo(() => parseTodos(props.input.todos))
return (
Expand Down
2 changes: 1 addition & 1 deletion packages/tui/src/routes/session/message-parts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const toolDisplays = new Set([
"subagent",
"execute",
"patch",
"todowrite",
"todo",
"question",
"skill",
])
Expand Down
5 changes: 3 additions & 2 deletions packages/tui/src/util/todo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
SessionMessageToolStateCompleted,
} from "@opencode/client"
import { isRecord } from "./record"
import { canonicalToolName } from "./tool-display"

export type TodoStatus = "pending" | "in_progress" | "completed" | "cancelled"

Expand All @@ -29,15 +30,15 @@ export function todoMarker(status: TodoStatus, ascii = false) {
return "[ ]"
}

/** The current list is the input of the latest completed `todowrite` call before the revert boundary. */
/** The current list is the input of the latest completed `todo` call before the revert boundary. */
export function sessionTodos(messages: ReadonlyArray<SessionMessageInfo>, boundary?: string): Todo[] {
const boundaryIndex = boundary ? messages.findIndex((message) => message.id === boundary) : -1
const tool = messages
.slice(0, boundaryIndex === -1 ? messages.length : boundaryIndex)
.flatMap((message) => (message.type === "assistant" ? message.content : []))
.findLast(
(item): item is SessionMessageAssistantTool & { state: SessionMessageToolStateCompleted } =>
item.type === "tool" && item.name === "todowrite" && item.state.status === "completed",
item.type === "tool" && canonicalToolName(item.name) === "todo" && item.state.status === "completed",
)
return parseTodos(tool?.state.input.todos)
}
1 change: 1 addition & 0 deletions packages/tui/src/util/tool-display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export function canonicalToolName(name: string) {
if (name === "bash") return "shell"
if (name === "task") return "subagent"
if (name === "apply_patch") return "patch"
if (name === "todowrite") return "todo"
return name
}

Expand Down
2 changes: 1 addition & 1 deletion services/www/src/docs/content/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ and cost considerations.

Experimental features may change or be removed in a future release.

Set `todo` to `true` to give agents a `todowrite` tool for tracking multi-step work.
Set `todo` to `true` to give agents a `todo` tool for tracking multi-step work.

```jsonc
{
Expand Down
5 changes: 3 additions & 2 deletions services/www/src/docs/content/migrate-v1.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,11 @@ V1 groups permission effects by tool. V2 uses one ordered `permissions` array, m
}
```

Permission actions also changed: `bash` is now `shell`, `task` is now `subagent`, and `write` and `patch` are now `edit`.
Permission actions also changed: `bash` is now `shell`, `task` is now `subagent`, `todowrite` is now `todo`, and `write` and
`patch` are now `edit`.
See [Permissions](/permissions) for the ordered V2 rule format.

Todos are off by default in V2. To turn them on, use the experimental flag. It may change or be removed in a future
Todos are off by default in V2. To turn on the `todo` tool, use the experimental flag. It may change or be removed in a future
release; see [Experimental](/config#experimental).

```jsonc
Expand Down
Loading