refactor: remove todo tool (#35989)

This commit is contained in:
Aiden Cline 2026-07-09 00:13:48 -05:00 committed by GitHub
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

@ -495,7 +495,6 @@ async function subscribeSessionEvents() {
console.log("Subscribing to session events...") console.log("Subscribing to session events...")
const TOOL: Record<string, [string, string]> = { const TOOL: Record<string, [string, string]> = {
todowrite: ["Todo", "\x1b[33m\x1b[1m"],
bash: ["Bash", "\x1b[31m\x1b[1m"], bash: ["Bash", "\x1b[31m\x1b[1m"],
edit: ["Edit", "\x1b[32m\x1b[1m"], edit: ["Edit", "\x1b[32m\x1b[1m"],
glob: ["Glob", "\x1b[34m\x1b[1m"], glob: ["Glob", "\x1b[34m\x1b[1m"],

View file

@ -33,11 +33,9 @@ test.describe("timeline tool state stability", () => {
} }
const names = { webfetch: "webfetch", websearch: "websearch", task: "task", skill: "skill", custom: "mcp_probe" } const names = { webfetch: "webfetch", websearch: "websearch", task: "task", skill: "skill", custom: "mcp_probe" }
const questionID = "prt_state_question" const questionID = "prt_state_question"
const todoID = "prt_state_todo"
const initial = [ const initial = [
...ids.map((id) => toolPart(`prt_state_${id}`, names[id], "pending", inputs[id])), ...ids.map((id) => toolPart(`prt_state_${id}`, names[id], "pending", inputs[id])),
toolPart(questionID, "question", "pending", questionInput()), toolPart(questionID, "question", "pending", questionInput()),
toolPart(todoID, "todowrite", "pending", { todos: [{ content: "Hidden", status: "pending" }] }),
textPart("prt_state_following", "Following lightweight tools"), textPart("prt_state_following", "Following lightweight tools"),
] ]
const childID = "ses_timeline_child" const childID = "ses_timeline_child"
@ -49,7 +47,6 @@ test.describe("timeline tool state stability", () => {
await timeline.send(status("busy"), 120) await timeline.send(status("busy"), 120)
for (const id of ids) await timeline.waitForPart(`prt_state_${id}`) for (const id of ids) await timeline.waitForPart(`prt_state_${id}`)
await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toHaveCount(0) await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toHaveCount(0)
await expect(page.locator(`[data-timeline-part-id="${todoID}"]`)).toHaveCount(0)
const regionIDs = [ const regionIDs = [
"prt_state_webfetch", "prt_state_webfetch",
@ -105,7 +102,6 @@ test.describe("timeline tool state stability", () => {
]), ]),
) )
await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toContainText("Keep it stable") await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toContainText("Keep it stable")
await expect(page.locator(`[data-timeline-part-id="${todoID}"]`)).toHaveCount(0)
await expect( await expect(
page.locator(`a[href$="/session/${childID}"]`, { has: page.locator('[data-component="task-tool-card"]') }), page.locator(`a[href$="/session/${childID}"]`, { has: page.locator('[data-component="task-tool-card"]') }),
).toBeVisible() ).toBeVisible()

View file

@ -269,7 +269,6 @@ const childMessages = Array.from({ length: 4 }, (_, index) => [
]).flat() ]).flat()
function renderable(part: MessagePart) { function renderable(part: MessagePart) {
if (part.type === "tool" && part.tool === "todowrite") return false
if (part.type === "text") return !!part.text.trim() if (part.type === "text") return !!part.text.trim()
if (part.type === "reasoning") return !!part.text.trim() if (part.type === "reasoning") return !!part.text.trim()
return part.type !== "step-start" && part.type !== "step-finish" && part.type !== "patch" return part.type !== "step-start" && part.type !== "step-finish" && part.type !== "patch"

View file

@ -90,7 +90,7 @@ async function mockServers(page: Page, requests: string[]) {
if (url.pathname === `/session/${current.id}`) return json(route, current) if (url.pathname === `/session/${current.id}`) return json(route, current)
if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404) if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404)
if (url.pathname === `/session/${current.id}/message`) return json(route, []) if (url.pathname === `/session/${current.id}/message`) return json(route, [])
if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(url.pathname)) return json(route, []) if (/^\/session\/[^/]+\/(children|diff)$/.test(url.pathname)) return json(route, [])
if (["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/diff"].includes(url.pathname)) if (["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/diff"].includes(url.pathname))
return json(route, []) return json(route, [])
if (["/global/config", "/config", "/provider/auth", "/mcp", "/session/status"].includes(url.pathname)) if (["/global/config", "/config", "/provider/auth", "/mcp", "/session/status"].includes(url.pathname))

View file

@ -65,7 +65,7 @@ async function mockServers(page: Page) {
if (url.pathname === `/session/${current.id}`) return json(route, current) if (url.pathname === `/session/${current.id}`) return json(route, current)
if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404) if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404)
if (url.pathname === `/session/${current.id}/message`) return json(route, []) if (url.pathname === `/session/${current.id}/message`) return json(route, [])
if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(url.pathname)) return json(route, []) if (/^\/session\/[^/]+\/(children|diff)$/.test(url.pathname)) return json(route, [])
if (["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/diff"].includes(url.pathname)) if (["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/diff"].includes(url.pathname))
return json(route, []) return json(route, [])
if (["/global/config", "/config", "/provider/auth", "/mcp"].includes(url.pathname)) return json(route, {}) if (["/global/config", "/config", "/provider/auth", "/mcp"].includes(url.pathname)) return json(route, {})

View file

@ -35,7 +35,6 @@ test.describe("session timeline projection", () => {
editPart("prt_edit"), editPart("prt_edit"),
toolPart("prt_write", "write", "completed", { filePath: "src/new.ts", content: "export const stable = true\n" }), toolPart("prt_write", "write", "completed", { filePath: "src/new.ts", content: "export const stable = true\n" }),
patchPart("prt_patch"), patchPart("prt_patch"),
toolPart("prt_todo", "todowrite", "completed", { todos: [{ content: "Hidden", status: "pending" }] }),
toolPart( toolPart(
"prt_question", "prt_question",
"question", "question",
@ -65,7 +64,6 @@ test.describe("session timeline projection", () => {
]) { ]) {
await expect(page.locator(`[data-timeline-part-id="${id}"]`).first(), id).toBeVisible() await expect(page.locator(`[data-timeline-part-id="${id}"]`).first(), id).toBeVisible()
} }
await expect(page.locator('[data-timeline-part-id="prt_todo"]')).toHaveCount(0)
}) })
test("projects gaps, dividers, assistant parts, and errors together", async ({ page }) => { test("projects gaps, dividers, assistant parts, and errors together", async ({ page }) => {

View file

@ -17,13 +17,11 @@ test("renders every tool error outcome without leaking hidden tools", async ({ p
error: "The user dismissed this question", error: "The user dismissed this question",
}), }),
toolPart("prt_question_error", "question", "error", questionInput(), { error: "Question transport failed" }), toolPart("prt_question_error", "question", "error", questionInput(), { error: "Question transport failed" }),
toolPart("prt_todo_error", "todowrite", "error", { todos: [] }, { error: "Hidden todo failure" }),
) )
await setupTimeline(page, { messages: [userMessage(), assistantMessage(parts)] }) await setupTimeline(page, { messages: [userMessage(), assistantMessage(parts)] })
await expect(page.locator('[data-kind="tool-error-card"]')).toHaveCount(ordinary.length + 1) await expect(page.locator('[data-kind="tool-error-card"]')).toHaveCount(ordinary.length + 1)
await expect(page.getByText(/dismissed/i)).toBeVisible() await expect(page.getByText(/dismissed/i)).toBeVisible()
await expect(page.locator('[data-timeline-part-id="prt_todo_error"]')).toHaveCount(0)
for (let index = 0; index < ordinary.length; index++) { for (let index = 0; index < ordinary.length; index++) {
await expect(page.locator(`[data-timeline-part-id="prt_error_${index}"]`)).toBeVisible() await expect(page.locator(`[data-timeline-part-id="prt_error_${index}"]`)).toBeVisible()
} }

View file

@ -1,186 +0,0 @@
import { base64Encode } from "@opencode-ai/core/util/encode"
import { expect, test, type Page } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectSessionTitle } from "../utils/waits"
const directory = "C:/OpenCode/TodoDockNavigation"
const projectID = "proj_todo_dock_navigation"
const sourceID = "ses_todo_dock_source"
const otherID = "ses_todo_dock_other"
const sourceTitle = "Todo dock animation"
const otherTitle = "Separate session"
const activeTodos = [
{ id: "todo-1", content: "Receive todos in the active session", status: "completed", priority: "high" },
{ id: "todo-2", content: "Keep the dock visible across tabs", status: "completed", priority: "high" },
{ id: "todo-3", content: "Close after the final todo", status: "in_progress", priority: "high" },
]
type EventPayload = {
directory: string
payload: Record<string, unknown>
}
test.use({ viewport: { width: 1440, height: 900 }, reducedMotion: "no-preference" })
test("animates todo lifecycle without replaying it across session tabs", async ({ page }) => {
test.setTimeout(90_000)
const events: EventPayload[] = []
const todos: Record<string, typeof activeTodos> = { [sourceID]: [], [otherID]: [] }
await mockOpenCodeServer(page, {
directory,
project: {
id: projectID,
worktree: directory,
vcs: "git",
name: "todo-dock-navigation",
time: { created: 1700000000000, updated: 1700000000000 },
sandboxes: [],
},
provider: {
all: [
{
id: "opencode",
name: "OpenCode",
models: {
"claude-opus-4-6": {
id: "claude-opus-4-6",
name: "Claude Opus 4.6",
limit: { context: 200_000 },
},
},
},
],
connected: ["opencode"],
default: { providerID: "opencode", modelID: "claude-opus-4-6" },
},
sessions: [session(sourceID, sourceTitle, 1700000000000), session(otherID, otherTitle, 1700000001000)],
pageMessages: () => ({ items: [] }),
events: () => events.splice(0, 1),
eventRetry: 16,
todos: (sessionID) => todos[sessionID] ?? [],
})
await configurePage(page)
await page.goto(sessionHref(sourceID))
await expectSessionTitle(page, sourceTitle)
const dock = page.locator('[data-component="session-todo-dock"]')
await expect(dock).toHaveCount(0)
events.push(statusEvent(sourceID, "busy"))
await expect(page.getByRole("button", { name: "Stop" })).toBeVisible()
await page.waitForTimeout(700)
const opening = sampleDock(page, 1_000)
todos[sourceID] = activeTodos
events.push(todoEvent(sourceID, activeTodos))
await expect(dock).toBeVisible()
await expect(dock.locator('[data-state="in_progress"]')).toHaveCount(1)
expect((await opening).some((sample) => sample.opacity > 0.05 && sample.opacity < 0.95)).toBe(true)
await switchSession(page, otherID, otherTitle)
await expect(dock).toHaveCount(0)
const returningOpen = sampleDock(page, 700)
await switchSession(page, sourceID, sourceTitle)
const openSamples = (await returningOpen).filter((sample) => sample.present)
expect(openSamples.length).toBeGreaterThan(0)
expect(openSamples[0]!.opacity).toBeGreaterThan(0.98)
expect(openSamples[0]!.height).toBeGreaterThan(70)
await expect(dock.locator('[data-state="in_progress"]')).toHaveCount(1)
const completedTodos = activeTodos.map((todo) => ({ ...todo, status: "completed" }))
const closing = sampleDock(page, 1_000)
todos[sourceID] = completedTodos
events.push(todoEvent(sourceID, completedTodos))
await expect(dock).toHaveCount(0)
expect((await closing).some((sample) => sample.opacity > 0.05 && sample.opacity < 0.95)).toBe(true)
todos[sourceID] = []
events.push(todoEvent(sourceID, []))
await switchSession(page, otherID, otherTitle)
const returningEmpty = sampleDock(page, 700)
await switchSession(page, sourceID, sourceTitle)
await expect(dock).toHaveCount(0)
expect((await returningEmpty).every((sample) => !sample.present)).toBe(true)
})
function session(id: string, title: string, created: number) {
return {
id,
slug: id,
projectID,
directory,
title,
version: "dev",
time: { created, updated: created },
}
}
function statusEvent(sessionID: string, type: "busy" | "idle"): EventPayload {
return {
directory,
payload: { type: "session.status", properties: { sessionID, status: { type } } },
}
}
function todoEvent(sessionID: string, next: typeof activeTodos): EventPayload {
return {
directory,
payload: { type: "todo.updated", properties: { sessionID, todos: next } },
}
}
async function configurePage(page: Page) {
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
await page.addInitScript(
({ directory, dirBase64, server, sessionIDs }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem(
"opencode.global.dat:server",
JSON.stringify({
projects: { local: [{ worktree: directory, expanded: true }] },
lastProject: { local: directory },
}),
)
localStorage.setItem(
"opencode.window.browser.dat:tabs",
JSON.stringify(sessionIDs.map((sessionId) => ({ type: "session", server, dirBase64, sessionId }))),
)
},
{ directory, dirBase64: base64Encode(directory), server, sessionIDs: [sourceID, otherID] },
)
}
function sessionHref(sessionID: string) {
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
return `/server/${base64Encode(server)}/session/${sessionID}`
}
async function switchSession(page: Page, sessionID: string, title: string) {
const href = sessionHref(sessionID)
const tab = page.locator(`[data-slot="titlebar-tabs"] a[href="${href}"]`).first()
await expect(tab).toBeVisible()
await tab.click()
await expectSessionTitle(page, title)
}
function sampleDock(page: Page, duration: number) {
return page.evaluate(async (duration) => {
const samples: { present: boolean; height: number; opacity: number }[] = []
const start = performance.now()
while (performance.now() - start < duration) {
const dock = document.querySelector<HTMLElement>('[data-component="session-todo-dock"]')
const clip = dock?.parentElement?.parentElement
const label = dock?.querySelector<HTMLElement>('[data-action="session-todo-toggle"] span[aria-label]')
samples.push({
present: !!dock,
height: clip?.getBoundingClientRect().height ?? 0,
opacity: label ? Number.parseFloat(getComputedStyle(label).opacity) : 0,
})
await new Promise(requestAnimationFrame)
}
return samples
}, duration)
}

View file

@ -63,7 +63,7 @@ async function mockServer(page: Page) {
if (byId) return json(route, byId) if (byId) return json(route, byId)
if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404) if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404)
if (/^\/session\/[^/]+\/message$/.test(url.pathname)) return json(route, []) if (/^\/session\/[^/]+\/message$/.test(url.pathname)) return json(route, [])
if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(url.pathname)) return json(route, []) if (/^\/session\/[^/]+\/(children|diff)$/.test(url.pathname)) return json(route, [])
if (["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/diff"].includes(url.pathname)) if (["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/diff"].includes(url.pathname))
return json(route, []) return json(route, [])
if (["/global/config", "/config", "/provider/auth", "/mcp", "/session/status"].includes(url.pathname)) if (["/global/config", "/config", "/provider/auth", "/mcp", "/session/status"].includes(url.pathname))

View file

@ -229,7 +229,6 @@ const sourceMessages = Array.from({ length: 12 }, (_, index) => [
]).flat() ]).flat()
function renderable(part: MessagePart) { function renderable(part: MessagePart) {
if (part.type === "tool" && part.tool === "todowrite") return false
if (part.type === "text") return !!part.text.trim() if (part.type === "text") return !!part.text.trim()
if (part.type === "reasoning") return !!part.text.trim() if (part.type === "reasoning") return !!part.text.trim()
return part.type !== "step-start" && part.type !== "step-finish" && part.type !== "patch" return part.type !== "step-start" && part.type !== "step-finish" && part.type !== "patch"

View file

@ -17,7 +17,6 @@ export interface MockServerConfig {
onMessage?: (input: { sessionID: string; messageID: string }) => void onMessage?: (input: { sessionID: string; messageID: string }) => void
events?: () => unknown[] events?: () => unknown[]
eventRetry?: number eventRetry?: number
todos?: (sessionID: string) => unknown[]
permissions?: unknown[] | (() => unknown[]) permissions?: unknown[] | (() => unknown[])
questions?: unknown[] | (() => unknown[]) questions?: unknown[] | (() => unknown[])
fileList?: (path: string) => unknown | Promise<unknown> fileList?: (path: string) => unknown | Promise<unknown>
@ -96,8 +95,6 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
return json(route, message) return json(route, message)
} }
const todoMatch = path.match(/^\/session\/([^/]+)\/todo$/)
if (todoMatch) return json(route, config.todos?.(todoMatch[1]!) ?? [])
if (/^\/session\/[^/]+\/(children|diff)$/.test(path)) return json(route, []) if (/^\/session\/[^/]+\/(children|diff)$/.test(path)) return json(route, [])
const messagesMatch = path.match(/^\/session\/([^/]+)\/message$/) const messagesMatch = path.match(/^\/session\/([^/]+)\/message$/)

View file

@ -1,8 +1,6 @@
// @ts-nocheck // @ts-nocheck
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import type { Todo } from "@opencode-ai/sdk/v2"
import { createPromptState } from "@/context/prompt" import { createPromptState } from "@/context/prompt"
import { SessionComposerRegion, createSessionComposerRegionController } from "@/pages/session/composer"
import { createPromptInputHistory, PromptInput } from "./prompt-input" import { createPromptInputHistory, PromptInput } from "./prompt-input"
function createPromptInputStoryRuntime() { function createPromptInputStoryRuntime() {
@ -104,94 +102,6 @@ function PromptInputExample() {
) )
} }
const todos: Todo[] = [
{ id: "todo-1", content: "Inspect the session composer animation", status: "completed" },
{ id: "todo-2", content: "Keep the dock settled on initial render", status: "in_progress" },
{ id: "todo-3", content: "Verify session navigation behavior", status: "pending" },
]
function PromptInputWithOpenDock() {
const input = createPromptInputStoryRuntime()
const [controls, setControls] = createStore({
agent: "build",
activeTab: undefined as string | undefined,
todoCollapsed: false,
})
const inputControls = {
agents: {
available: [],
options: ["build"],
get current() {
return controls.agent
},
loading: false,
visible: true,
select: (agent?: string) => setControls("agent", agent ?? "build"),
},
model: {
selection: {
current: () => ({ id: "claude-3-7-sonnet", name: "Claude 3.7 Sonnet", provider: { id: "anthropic" } }),
variant: { list: () => [], current: () => undefined, set: () => {} },
},
paid: true,
loading: false,
},
session: {
id: "story-session",
tabs: {
active: () => controls.activeTab,
all: () => [],
open: () => {},
setActive: (tab: string) => setControls("activeTab", tab),
},
reviewPanel: { opened: () => false, open: () => {} },
},
newLayoutDesigns: true,
}
const state = {
blocked: () => false,
questionRequest: () => undefined,
permissionRequest: () => undefined,
permissionResponding: () => false,
decide: () => {},
todos: () => todos,
dock: () => true,
closing: () => false,
opening: () => false,
}
return (
<SessionComposerRegion
controller={createSessionComposerRegionController({
state,
sessionKey: () => "story-session",
sessionID: () => "story-session",
prompt: input.state,
ready: () => true,
centered: () => false,
todo: {
collapsed: () => controls.todoCollapsed,
onToggle: () => setControls("todoCollapsed", (collapsed) => !collapsed),
},
followup: () => undefined,
revert: () => undefined,
onResponseSubmit: () => {},
openParent: () => {},
setPromptRef: () => {},
setDockRef: () => {},
})}
promptInput={
<PromptInput
controls={inputControls}
{...input}
ref={() => {}}
newSessionWorktree=""
onNewSessionWorktreeReset={() => {}}
/>
}
/>
)
}
export default { export default {
title: "App/PromptInput", title: "App/PromptInput",
id: "app-prompt-input", id: "app-prompt-input",
@ -206,12 +116,3 @@ export const Basic = {
</div> </div>
), ),
} }
export const DockAlreadyOpen = {
render: () => (
<div class="pt-10">
<h1 class="mb-4">Prompt Input with open Todo dock</h1>
<PromptInputWithOpenDock />
</div>
),
}

View file

@ -221,8 +221,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
const sessionID = params.id const sessionID = params.id
if (!sessionID) return Promise.resolve() if (!sessionID) return Promise.resolve()
serverSync().session.set("todo", sessionID, [])
input.onAbort?.() input.onAbort?.()
const key = pendingKey(sessionID) const key = pendingKey(sessionID)

View file

@ -11,7 +11,6 @@ const sessionFields = new Set([
"session_status", "session_status",
"session_working", "session_working",
"session_diff", "session_diff",
"todo",
"permission", "permission",
"question", "question",
"message", "message",
@ -115,7 +114,6 @@ export const createDirSyncContext = (
index(sessionID) index(sessionID)
}, },
diff: serverSync.session.diff, diff: serverSync.session.diff,
todo: serverSync.session.todo,
history: serverSync.session.history, history: serverSync.session.history,
evict(sessionID: string) { evict(sessionID: string) {
serverSync.session.evict(sessionID) serverSync.session.evict(sessionID)

View file

@ -30,7 +30,6 @@ function directoryState() {
return this.session_status[id]?.type !== "idle" return this.session_status[id]?.type !== "idle"
}, },
session_diff: {}, session_diff: {},
todo: {},
permission: {}, permission: {},
question: {}, question: {},
mcp_ready: true, mcp_ready: true,

View file

@ -223,7 +223,6 @@ export function createChildStoreManager(input: {
return (type ?? "idle") !== "idle" return (type ?? "idle") !== "idle"
}, },
session_diff: {}, session_diff: {},
todo: {},
permission: {}, permission: {},
question: {}, question: {},
get mcp_ready() { get mcp_ready() {

View file

@ -72,7 +72,6 @@ const baseState = (input: Partial<State> = {}) =>
sessionTotal: 0, sessionTotal: 0,
session_status: {}, session_status: {},
session_diff: {}, session_diff: {},
todo: {},
permission: {}, permission: {},
question: {}, question: {},
mcp: {}, mcp: {},
@ -216,7 +215,6 @@ describe("applyDirectoryEvent", () => {
message: { ses_1: [message] }, message: { ses_1: [message] },
part: { [message.id]: [textPart("prt_1", "ses_1", message.id)] }, part: { [message.id]: [textPart("prt_1", "ses_1", message.id)] },
session_diff: { ses_1: [] }, session_diff: { ses_1: [] },
todo: { ses_1: [] },
permission: { ses_1: [] }, permission: { ses_1: [] },
question: { ses_1: [] }, question: { ses_1: [] },
session_status: { ses_1: { type: "busy" } }, session_status: { ses_1: { type: "busy" } },
@ -237,7 +235,6 @@ describe("applyDirectoryEvent", () => {
expect(store.message.ses_1).toBeUndefined() expect(store.message.ses_1).toBeUndefined()
expect(store.part[message.id]).toBeUndefined() expect(store.part[message.id]).toBeUndefined()
expect(store.session_diff.ses_1).toBeUndefined() expect(store.session_diff.ses_1).toBeUndefined()
expect(store.todo.ses_1).toBeUndefined()
expect(store.permission.ses_1).toBeUndefined() expect(store.permission.ses_1).toBeUndefined()
expect(store.question.ses_1).toBeUndefined() expect(store.question.ses_1).toBeUndefined()
expect(store.session_status.ses_1).toBeUndefined() expect(store.session_status.ses_1).toBeUndefined()
@ -262,7 +259,6 @@ describe("applyDirectoryEvent", () => {
message: { [item.info.id]: [message] }, message: { [item.info.id]: [message] },
part: { [message.id]: [textPart("prt_1", item.info.id, message.id)] }, part: { [message.id]: [textPart("prt_1", item.info.id, message.id)] },
session_diff: { [item.info.id]: [] }, session_diff: { [item.info.id]: [] },
todo: { [item.info.id]: [] },
permission: { [item.info.id]: [] }, permission: { [item.info.id]: [] },
question: { [item.info.id]: [] }, question: { [item.info.id]: [] },
session_status: { [item.info.id]: { type: "busy" } }, session_status: { [item.info.id]: { type: "busy" } },
@ -283,7 +279,6 @@ describe("applyDirectoryEvent", () => {
expect(store.message[item.info.id]).toBeUndefined() expect(store.message[item.info.id]).toBeUndefined()
expect(store.part[message.id]).toBeUndefined() expect(store.part[message.id]).toBeUndefined()
expect(store.session_diff[item.info.id]).toBeUndefined() expect(store.session_diff[item.info.id]).toBeUndefined()
expect(store.todo[item.info.id]).toBeUndefined()
expect(store.permission[item.info.id]).toBeUndefined() expect(store.permission[item.info.id]).toBeUndefined()
expect(store.question[item.info.id]).toBeUndefined() expect(store.question[item.info.id]).toBeUndefined()
expect(store.session_status[item.info.id]).toBeUndefined() expect(store.session_status[item.info.id]).toBeUndefined()
@ -294,7 +289,6 @@ describe("applyDirectoryEvent", () => {
const dropped = rootSession({ id: "ses_b" }) const dropped = rootSession({ id: "ses_b" })
const kept = rootSession({ id: "ses_a" }) const kept = rootSession({ id: "ses_a" })
const message = userMessage("msg_1", dropped.id) const message = userMessage("msg_1", dropped.id)
const todos: string[] = []
const [store, setStore] = createStore( const [store, setStore] = createStore(
baseState({ baseState({
limit: 1, limit: 1,
@ -302,7 +296,6 @@ describe("applyDirectoryEvent", () => {
message: { [dropped.id]: [message] }, message: { [dropped.id]: [message] },
part: { [message.id]: [textPart("prt_1", dropped.id, message.id)] }, part: { [message.id]: [textPart("prt_1", dropped.id, message.id)] },
session_diff: { [dropped.id]: [] }, session_diff: { [dropped.id]: [] },
todo: { [dropped.id]: [] },
permission: { [dropped.id]: [] }, permission: { [dropped.id]: [] },
question: { [dropped.id]: [] }, question: { [dropped.id]: [] },
session_status: { [dropped.id]: { type: "busy" } }, session_status: { [dropped.id]: { type: "busy" } },
@ -316,21 +309,15 @@ describe("applyDirectoryEvent", () => {
push() {}, push() {},
directory: "/tmp", directory: "/tmp",
loadLsp() {}, loadLsp() {},
setSessionTodo(sessionID, value) {
if (value !== undefined) return
todos.push(sessionID)
},
}) })
expect(store.session.map((x) => x.id)).toEqual([kept.id]) expect(store.session.map((x) => x.id)).toEqual([kept.id])
expect(store.message[dropped.id]).toBeUndefined() expect(store.message[dropped.id]).toBeUndefined()
expect(store.part[message.id]).toBeUndefined() expect(store.part[message.id]).toBeUndefined()
expect(store.session_diff[dropped.id]).toBeUndefined() expect(store.session_diff[dropped.id]).toBeUndefined()
expect(store.todo[dropped.id]).toBeUndefined()
expect(store.permission[dropped.id]).toBeUndefined() expect(store.permission[dropped.id]).toBeUndefined()
expect(store.question[dropped.id]).toBeUndefined() expect(store.question[dropped.id]).toBeUndefined()
expect(store.session_status[dropped.id]).toBeUndefined() expect(store.session_status[dropped.id]).toBeUndefined()
expect(todos).toEqual([dropped.id])
}) })
test("cleanupDroppedSessionCaches clears part-only orphan state", () => { test("cleanupDroppedSessionCaches clears part-only orphan state", () => {

View file

@ -9,7 +9,6 @@ import type {
Session, Session,
SessionStatus, SessionStatus,
FileDiffInfo, FileDiffInfo,
Todo,
} from "@opencode-ai/sdk/v2/client" } from "@opencode-ai/sdk/v2/client"
import type { State, VcsCache } from "./types" import type { State, VcsCache } from "./types"
import { trimSessions } from "./session-trim" import { trimSessions } from "./session-trim"
@ -19,7 +18,6 @@ import { diffs as list, message as clean } from "@/utils/diffs"
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"]) const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
const SESSION_CONTENT_EVENTS = new Set([ const SESSION_CONTENT_EVENTS = new Set([
"session.diff", "session.diff",
"todo.updated",
"session.status", "session.status",
"message.updated", "message.updated",
"message.removed", "message.removed",
@ -62,13 +60,8 @@ export function applyGlobalEvent(input: {
) )
} }
function cleanupSessionCaches( function cleanupSessionCaches(setStore: SetStoreFunction<State>, sessionID: string) {
setStore: SetStoreFunction<State>,
sessionID: string,
setSessionTodo?: (sessionID: string, todos: Todo[] | undefined) => void,
) {
if (!sessionID) return if (!sessionID) return
setSessionTodo?.(sessionID, undefined)
setStore( setStore(
produce((draft) => { produce((draft) => {
dropSessionCaches(draft, [sessionID]) dropSessionCaches(draft, [sessionID])
@ -76,17 +69,11 @@ function cleanupSessionCaches(
) )
} }
export function cleanupDroppedSessionCaches( export function cleanupDroppedSessionCaches(store: Store<State>, setStore: SetStoreFunction<State>, next: Session[]) {
store: Store<State>,
setStore: SetStoreFunction<State>,
next: Session[],
setSessionTodo?: (sessionID: string, todos: Todo[] | undefined) => void,
) {
const keep = new Set(next.map((item) => item.id)) const keep = new Set(next.map((item) => item.id))
const stale = [ const stale = [
...Object.keys(store.message), ...Object.keys(store.message),
...Object.keys(store.session_diff), ...Object.keys(store.session_diff),
...Object.keys(store.todo),
...Object.keys(store.permission), ...Object.keys(store.permission),
...Object.keys(store.question), ...Object.keys(store.question),
...Object.keys(store.session_status), ...Object.keys(store.session_status),
@ -95,9 +82,6 @@ export function cleanupDroppedSessionCaches(
.filter((sessionID): sessionID is string => !!sessionID), .filter((sessionID): sessionID is string => !!sessionID),
].filter((sessionID, index, list) => !keep.has(sessionID) && list.indexOf(sessionID) === index) ].filter((sessionID, index, list) => !keep.has(sessionID) && list.indexOf(sessionID) === index)
if (stale.length === 0) return if (stale.length === 0) return
for (const sessionID of stale) {
setSessionTodo?.(sessionID, undefined)
}
setStore( setStore(
produce((draft) => { produce((draft) => {
dropSessionCaches(draft, stale) dropSessionCaches(draft, stale)
@ -114,7 +98,6 @@ export function applyDirectoryEvent(input: {
loadLsp: () => void loadLsp: () => void
loadReferences?: () => void loadReferences?: () => void
vcsCache?: VcsCache vcsCache?: VcsCache
setSessionTodo?: (sessionID: string, todos: Todo[] | undefined) => void
retainedLimit?: number retainedLimit?: number
sessionContent?: boolean sessionContent?: boolean
permission?: State["permission"] permission?: State["permission"]
@ -138,7 +121,7 @@ export function applyDirectoryEvent(input: {
next.splice(result.index, 0, info) next.splice(result.index, 0, info)
const trimmed = trimSessions(next, { limit, permission: input.permission ?? input.store.permission }) const trimmed = trimSessions(next, { limit, permission: input.permission ?? input.store.permission })
input.setStore("session", reconcile(trimmed, { key: "id" })) input.setStore("session", reconcile(trimmed, { key: "id" }))
cleanupDroppedSessionCaches(input.store, input.setStore, trimmed, input.setSessionTodo) cleanupDroppedSessionCaches(input.store, input.setStore, trimmed)
if (!info.parentID) input.setStore("sessionTotal", (value) => value + 1) if (!info.parentID) input.setStore("sessionTotal", (value) => value + 1)
break break
} }
@ -155,7 +138,7 @@ export function applyDirectoryEvent(input: {
}), }),
) )
} }
cleanupSessionCaches(input.setStore, info.id, input.setSessionTodo) cleanupSessionCaches(input.setStore, info.id)
if (info.parentID) break if (info.parentID) break
input.setStore("sessionTotal", (value) => Math.max(0, value - 1)) input.setStore("sessionTotal", (value) => Math.max(0, value - 1))
break break
@ -168,7 +151,7 @@ export function applyDirectoryEvent(input: {
next.splice(result.index, 0, info) next.splice(result.index, 0, info)
const trimmed = trimSessions(next, { limit, permission: input.permission ?? input.store.permission }) const trimmed = trimSessions(next, { limit, permission: input.permission ?? input.store.permission })
input.setStore("session", reconcile(trimmed, { key: "id" })) input.setStore("session", reconcile(trimmed, { key: "id" }))
cleanupDroppedSessionCaches(input.store, input.setStore, trimmed, input.setSessionTodo) cleanupDroppedSessionCaches(input.store, input.setStore, trimmed)
break break
} }
case "session.deleted": { case "session.deleted": {
@ -182,7 +165,7 @@ export function applyDirectoryEvent(input: {
}), }),
) )
} }
cleanupSessionCaches(input.setStore, info.id, input.setSessionTodo) cleanupSessionCaches(input.setStore, info.id)
if (info.parentID) break if (info.parentID) break
input.setStore("sessionTotal", (value) => Math.max(0, value - 1)) input.setStore("sessionTotal", (value) => Math.max(0, value - 1))
break break
@ -192,12 +175,6 @@ export function applyDirectoryEvent(input: {
input.setStore("session_diff", props.sessionID, reconcile(list(props.diff), { key: "file" })) input.setStore("session_diff", props.sessionID, reconcile(list(props.diff), { key: "file" }))
break break
} }
case "todo.updated": {
const props = event.properties as { sessionID: string; todos: Todo[] }
input.setStore("todo", props.sessionID, reconcile(props.todos, { key: "id" }))
input.setSessionTodo?.(props.sessionID, props.todos)
break
}
case "session.status": { case "session.status": {
const props = event.properties as { sessionID: string; status: SessionStatus } const props = event.properties as { sessionID: string; status: SessionStatus }
input.setStore("session_status", props.sessionID, reconcile(props.status)) input.setStore("session_status", props.sessionID, reconcile(props.status))

View file

@ -6,7 +6,6 @@ import type {
QuestionRequest, QuestionRequest,
SessionStatus, SessionStatus,
FileDiffInfo, FileDiffInfo,
Todo,
} from "@opencode-ai/sdk/v2/client" } from "@opencode-ai/sdk/v2/client"
import { dropSessionCaches, pickSessionCacheEvictions } from "./session-cache" import { dropSessionCaches, pickSessionCacheEvictions } from "./session-cache"
@ -34,7 +33,6 @@ describe("app session cache", () => {
const store: { const store: {
session_status: Record<string, SessionStatus | undefined> session_status: Record<string, SessionStatus | undefined>
session_diff: Record<string, FileDiffInfo[] | undefined> session_diff: Record<string, FileDiffInfo[] | undefined>
todo: Record<string, Todo[] | undefined>
message: Record<string, Message[] | undefined> message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined> part: Record<string, Part[] | undefined>
permission: Record<string, PermissionRequest[] | undefined> permission: Record<string, PermissionRequest[] | undefined>
@ -43,7 +41,6 @@ describe("app session cache", () => {
} = { } = {
session_status: { ses_1: { type: "busy" } as SessionStatus }, session_status: { ses_1: { type: "busy" } as SessionStatus },
session_diff: { ses_1: [] }, session_diff: { ses_1: [] },
todo: { ses_1: [] as Todo[] },
message: {}, message: {},
part: { msg_1: [part("prt_1", "ses_1", "msg_1")] }, part: { msg_1: [part("prt_1", "ses_1", "msg_1")] },
permission: { ses_1: [] as PermissionRequest[] }, permission: { ses_1: [] as PermissionRequest[] },
@ -56,7 +53,6 @@ describe("app session cache", () => {
expect(store.message.ses_1).toBeUndefined() expect(store.message.ses_1).toBeUndefined()
expect(store.part.msg_1).toBeUndefined() expect(store.part.msg_1).toBeUndefined()
expect(store.part_text_accum_delta.prt_1).toBeUndefined() expect(store.part_text_accum_delta.prt_1).toBeUndefined()
expect(store.todo.ses_1).toBeUndefined()
expect(store.session_diff.ses_1).toBeUndefined() expect(store.session_diff.ses_1).toBeUndefined()
expect(store.session_status.ses_1).toBeUndefined() expect(store.session_status.ses_1).toBeUndefined()
expect(store.permission.ses_1).toBeUndefined() expect(store.permission.ses_1).toBeUndefined()
@ -68,7 +64,6 @@ describe("app session cache", () => {
const store: { const store: {
session_status: Record<string, SessionStatus | undefined> session_status: Record<string, SessionStatus | undefined>
session_diff: Record<string, FileDiffInfo[] | undefined> session_diff: Record<string, FileDiffInfo[] | undefined>
todo: Record<string, Todo[] | undefined>
message: Record<string, Message[] | undefined> message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined> part: Record<string, Part[] | undefined>
permission: Record<string, PermissionRequest[] | undefined> permission: Record<string, PermissionRequest[] | undefined>
@ -77,7 +72,6 @@ describe("app session cache", () => {
} = { } = {
session_status: {}, session_status: {},
session_diff: {}, session_diff: {},
todo: {},
message: { ses_1: [m] }, message: { ses_1: [m] },
part: { [m.id]: [part("prt_1", "ses_1", m.id)] }, part: { [m.id]: [part("prt_1", "ses_1", m.id)] },
permission: {}, permission: {},

View file

@ -5,7 +5,6 @@ import type {
QuestionRequest, QuestionRequest,
SessionStatus, SessionStatus,
FileDiffInfo, FileDiffInfo,
Todo,
} from "@opencode-ai/sdk/v2/client" } from "@opencode-ai/sdk/v2/client"
export const SESSION_CACHE_LIMIT = 40 export const SESSION_CACHE_LIMIT = 40
@ -13,7 +12,6 @@ export const SESSION_CACHE_LIMIT = 40
type SessionCache = { type SessionCache = {
session_status: Record<string, SessionStatus | undefined> session_status: Record<string, SessionStatus | undefined>
session_diff: Record<string, FileDiffInfo[] | undefined> session_diff: Record<string, FileDiffInfo[] | undefined>
todo: Record<string, Todo[] | undefined>
message: Record<string, Message[] | undefined> message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined> part: Record<string, Part[] | undefined>
permission: Record<string, PermissionRequest[] | undefined> permission: Record<string, PermissionRequest[] | undefined>
@ -36,7 +34,6 @@ export function dropSessionCaches(store: SessionCache, sessionIDs: Iterable<stri
for (const sessionID of stale) { for (const sessionID of stale) {
delete store.message[sessionID] delete store.message[sessionID]
delete store.todo[sessionID]
delete store.session_diff[sessionID] delete store.session_diff[sessionID]
delete store.session_status[sessionID] delete store.session_status[sessionID]
delete store.permission[sessionID] delete store.permission[sessionID]

View file

@ -14,7 +14,6 @@ import type {
Session, Session,
SessionStatus, SessionStatus,
FileDiffInfo, FileDiffInfo,
Todo,
VcsInfo, VcsInfo,
} from "@opencode-ai/sdk/v2/client" } from "@opencode-ai/sdk/v2/client"
import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context" import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
@ -53,9 +52,6 @@ export type State = {
session_diff: { session_diff: {
[sessionID: string]: FileDiffInfo[] [sessionID: string]: FileDiffInfo[]
} }
todo: {
[sessionID: string]: Todo[]
}
permission: { permission: {
[sessionID: string]: PermissionRequest[] [sessionID: string]: PermissionRequest[]
} }

View file

@ -65,7 +65,6 @@ type SessionView = {
reviewOpen?: string[] reviewOpen?: string[]
pendingMessage?: string pendingMessage?: string
pendingMessageAt?: number pendingMessageAt?: number
todoCollapsed?: boolean
} }
type TabHandoff = { type TabHandoff = {
@ -836,18 +835,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setScroll(tab: string, pos: SessionScroll) { setScroll(tab: string, pos: SessionScroll) {
scroll.setScroll(key(), tab, pos) scroll.setScroll(key(), tab, pos)
}, },
todoCollapsed: {
get: () => s().todoCollapsed ?? false,
set(collapsed: boolean) {
const session = key()
const current = store.sessionView[session]
if (!current) {
setStore("sessionView", session, { scroll: {}, todoCollapsed: collapsed })
} else {
setStore("sessionView", session, "todoCollapsed", collapsed)
}
},
},
terminal: { terminal: {
opened: terminalOpened, opened: terminalOpened,
open() { open() {

View file

@ -151,7 +151,6 @@ function setup(sessions: Record<string, Session>) {
return response() return response()
}, },
diff: async () => ({ data: [] }), diff: async () => ({ data: [] }),
todo: async () => ({ data: [] }),
}, },
} as unknown as OpencodeClient } as unknown as OpencodeClient
return { get, messages, store: createServerSession(client) } return { get, messages, store: createServerSession(client) }

View file

@ -9,7 +9,6 @@ import type {
Session, Session,
SessionStatus, SessionStatus,
FileDiffInfo, FileDiffInfo,
Todo,
} from "@opencode-ai/sdk/v2/client" } from "@opencode-ai/sdk/v2/client"
import { batch } from "solid-js" import { batch } from "solid-js"
import { createStore, produce, reconcile } from "solid-js/store" import { createStore, produce, reconcile } from "solid-js/store"
@ -140,7 +139,6 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
info: {} as Record<string, Session | undefined>, info: {} as Record<string, Session | undefined>,
session_status: {} as Record<string, SessionStatus>, session_status: {} as Record<string, SessionStatus>,
session_diff: {} as Record<string, FileDiffInfo[]>, session_diff: {} as Record<string, FileDiffInfo[]>,
todo: {} as Record<string, Todo[]>,
permission: {} as Record<string, PermissionRequest[]>, permission: {} as Record<string, PermissionRequest[]>,
question: {} as Record<string, QuestionRequest[]>, question: {} as Record<string, QuestionRequest[]>,
message: {} as Record<string, Message[]>, message: {} as Record<string, Message[]>,
@ -153,7 +151,6 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
const requests = new Map<string, Promise<Session>>() const requests = new Map<string, Promise<Session>>()
const inflight = new Map<string, Promise<void>>() const inflight = new Map<string, Promise<void>>()
const inflightDiff = new Map<string, Promise<void>>() const inflightDiff = new Map<string, Promise<void>>()
const inflightTodo = new Map<string, Promise<void>>()
const optimistic = new Map<string, Map<string, OptimisticItem>>() const optimistic = new Map<string, Map<string, OptimisticItem>>()
const messageLoads = new Map<string, MessageLoadState>() const messageLoads = new Map<string, MessageLoadState>()
const pendingParts = new Map<string, Map<string, Set<string>>>() const pendingParts = new Map<string, Map<string, Set<string>>>()
@ -199,7 +196,6 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
...requests.keys(), ...requests.keys(),
...inflight.keys(), ...inflight.keys(),
...inflightDiff.keys(), ...inflightDiff.keys(),
...inflightTodo.keys(),
...messageLoads.keys(), ...messageLoads.keys(),
...optimistic.keys(), ...optimistic.keys(),
...Object.entries(data.permission) ...Object.entries(data.permission)
@ -254,8 +250,7 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
!requests.has(sessionID) && !requests.has(sessionID) &&
!messageLoads.has(sessionID) && !messageLoads.has(sessionID) &&
!inflight.has(sessionID) && !inflight.has(sessionID) &&
!inflightDiff.has(sessionID) && !inflightDiff.has(sessionID)
!inflightTodo.has(sessionID)
) )
generations.delete(sessionID) generations.delete(sessionID)
} }
@ -418,7 +413,6 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
requests.delete(sessionID) requests.delete(sessionID)
inflight.delete(sessionID) inflight.delete(sessionID)
inflightDiff.delete(sessionID) inflightDiff.delete(sessionID)
inflightTodo.delete(sessionID)
messageLoads.delete(sessionID) messageLoads.delete(sessionID)
pendingParts.delete(sessionID) pendingParts.delete(sessionID)
orphanParts.delete(sessionID) orphanParts.delete(sessionID)
@ -448,7 +442,6 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
...requests.keys(), ...requests.keys(),
...inflight.keys(), ...inflight.keys(),
...inflightDiff.keys(), ...inflightDiff.keys(),
...inflightTodo.keys(),
...messageLoads.keys(), ...messageLoads.keys(),
...optimistic.keys(), ...optimistic.keys(),
...Object.entries(data.permission) ...Object.entries(data.permission)
@ -773,11 +766,6 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
setData("session_diff", props.sessionID, reconcile(cleanDiffs(props.diff), { key: "file" })) setData("session_diff", props.sessionID, reconcile(cleanDiffs(props.diff), { key: "file" }))
return return
} }
case "todo.updated": {
const props = event.properties as { sessionID: string; todos: Todo[] }
setData("todo", props.sessionID, reconcile(props.todos, { key: "id" }))
return
}
case "session.status": { case "session.status": {
const props = event.properties as { sessionID: string; status: SessionStatus } const props = event.properties as { sessionID: string; status: SessionStatus }
setData("session_status", props.sessionID, reconcile(props.status)) setData("session_status", props.sessionID, reconcile(props.status))
@ -1140,17 +1128,6 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
}) })
}) })
}, },
todo(sessionID: string, options?: { force?: boolean }) {
touch(sessionID)
if (data.todo[sessionID] !== undefined && !options?.force) return Promise.resolve()
return runInflight(inflightTodo, sessionID, () => {
const active = generation(sessionID)
return retry(() => client.session.todo({ sessionID })).then((result) => {
if (generations.get(sessionID) !== active) return
setData("todo", sessionID, reconcile(result.data ?? [], { key: "id" }))
})
})
},
history: { history: {
more: (sessionID: string) => more: (sessionID: string) =>
data.message[sessionID] !== undefined && data.message[sessionID] !== undefined &&

View file

@ -577,9 +577,6 @@ export const dict = {
"session.messages.loading": "جارٍ تحميل الرسائل...", "session.messages.loading": "جارٍ تحميل الرسائل...",
"session.messages.jumpToLatest": "الانتقال إلى الأحدث", "session.messages.jumpToLatest": "الانتقال إلى الأحدث",
"session.context.addToContext": "إضافة {{selection}} إلى السياق", "session.context.addToContext": "إضافة {{selection}} إلى السياق",
"session.todo.title": "المهام",
"session.todo.collapse": "طي",
"session.todo.expand": "توسيع",
"session.question.minimize": "تصغير السؤال", "session.question.minimize": "تصغير السؤال",
"session.question.restore": "استعادة السؤال", "session.question.restore": "استعادة السؤال",
"session.question.pending.one": "{{count}} سؤال معلق", "session.question.pending.one": "{{count}} سؤال معلق",
@ -867,8 +864,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "تحميل مهارة بالاسم", "settings.permissions.tool.skill.description": "تحميل مهارة بالاسم",
"settings.permissions.tool.lsp.title": "LSP", "settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "تشغيل استعلامات خادم اللغة", "settings.permissions.tool.lsp.description": "تشغيل استعلامات خادم اللغة",
"settings.permissions.tool.todowrite.title": "كتابة المهام",
"settings.permissions.tool.todowrite.description": "تحديث قائمة المهام",
"settings.permissions.tool.webfetch.title": "جلب الويب", "settings.permissions.tool.webfetch.title": "جلب الويب",
"settings.permissions.tool.webfetch.description": "جلب محتوى من عنوان URL", "settings.permissions.tool.webfetch.description": "جلب محتوى من عنوان URL",
"settings.permissions.tool.websearch.title": "بحث الويب", "settings.permissions.tool.websearch.title": "بحث الويب",
@ -931,7 +926,6 @@ export const dict = {
"session.review.noVcs.createGit.description": "تتبع ومراجعة والتراجع عن التغييرات في هذا المشروع", "session.review.noVcs.createGit.description": "تتبع ومراجعة والتراجع عن التغييرات في هذا المشروع",
"session.review.noVcs.createGit.actionLoading": "جاري إنشاء مستودع Git...", "session.review.noVcs.createGit.actionLoading": "جاري إنشاء مستودع Git...",
"session.review.noVcs.createGit.action": "إنشاء مستودع Git", "session.review.noVcs.createGit.action": "إنشاء مستودع Git",
"session.todo.progress": "تم إكمال {{done}} من {{total}} مهام",
"session.question.progress": "{{current}} من {{total}} أسئلة", "session.question.progress": "{{current}} من {{total}} أسئلة",
"session.header.open.finder": "Finder", "session.header.open.finder": "Finder",
"session.header.open.fileExplorer": "مستكشف الملفات", "session.header.open.fileExplorer": "مستكشف الملفات",

View file

@ -582,9 +582,6 @@ export const dict = {
"session.messages.loading": "Carregando mensagens...", "session.messages.loading": "Carregando mensagens...",
"session.messages.jumpToLatest": "Ir para a mais recente", "session.messages.jumpToLatest": "Ir para a mais recente",
"session.context.addToContext": "Adicionar {{selection}} ao contexto", "session.context.addToContext": "Adicionar {{selection}} ao contexto",
"session.todo.title": "Tarefas",
"session.todo.collapse": "Recolher",
"session.todo.expand": "Expandir",
"session.question.minimize": "Minimizar pergunta", "session.question.minimize": "Minimizar pergunta",
"session.question.restore": "Restaurar pergunta", "session.question.restore": "Restaurar pergunta",
"session.question.pending.one": "{{count}} pergunta pendente", "session.question.pending.one": "{{count}} pergunta pendente",
@ -880,8 +877,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "Carregar uma habilidade por nome", "settings.permissions.tool.skill.description": "Carregar uma habilidade por nome",
"settings.permissions.tool.lsp.title": "LSP", "settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "Executar consultas de servidor de linguagem", "settings.permissions.tool.lsp.description": "Executar consultas de servidor de linguagem",
"settings.permissions.tool.todowrite.title": "Escrever Tarefas",
"settings.permissions.tool.todowrite.description": "Atualizar a lista de tarefas",
"settings.permissions.tool.webfetch.title": "Buscar Web", "settings.permissions.tool.webfetch.title": "Buscar Web",
"settings.permissions.tool.webfetch.description": "Buscar conteúdo de uma URL", "settings.permissions.tool.webfetch.description": "Buscar conteúdo de uma URL",
"settings.permissions.tool.websearch.title": "Pesquisa Web", "settings.permissions.tool.websearch.title": "Pesquisa Web",
@ -944,7 +939,6 @@ export const dict = {
"session.review.noVcs.createGit.description": "Rastreie, revise e desfaça alterações neste projeto", "session.review.noVcs.createGit.description": "Rastreie, revise e desfaça alterações neste projeto",
"session.review.noVcs.createGit.actionLoading": "Criando repositório Git...", "session.review.noVcs.createGit.actionLoading": "Criando repositório Git...",
"session.review.noVcs.createGit.action": "Criar repositório Git", "session.review.noVcs.createGit.action": "Criar repositório Git",
"session.todo.progress": "{{done}} de {{total}} tarefas concluídas",
"session.question.progress": "{{current}} de {{total}} perguntas", "session.question.progress": "{{current}} de {{total}} perguntas",
"session.header.open.finder": "Finder", "session.header.open.finder": "Finder",
"session.header.open.fileExplorer": "Explorador de Arquivos", "session.header.open.fileExplorer": "Explorador de Arquivos",

View file

@ -637,9 +637,6 @@ export const dict = {
"session.messages.jumpToLatest": "Idi na najnovije", "session.messages.jumpToLatest": "Idi na najnovije",
"session.context.addToContext": "Dodaj {{selection}} u kontekst", "session.context.addToContext": "Dodaj {{selection}} u kontekst",
"session.todo.title": "Zadaci",
"session.todo.collapse": "Sažmi",
"session.todo.expand": "Proširi",
"session.question.minimize": "Minimiziraj pitanje", "session.question.minimize": "Minimiziraj pitanje",
"session.question.restore": "Vrati pitanje", "session.question.restore": "Vrati pitanje",
"session.question.pending.one": "{{count}} pitanje na čekanju", "session.question.pending.one": "{{count}} pitanje na čekanju",
@ -954,8 +951,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "Učitaj vještinu po nazivu", "settings.permissions.tool.skill.description": "Učitaj vještinu po nazivu",
"settings.permissions.tool.lsp.title": "LSP", "settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "Pokreni upite jezičnog servera", "settings.permissions.tool.lsp.description": "Pokreni upite jezičnog servera",
"settings.permissions.tool.todowrite.title": "Ažuriranje liste zadataka",
"settings.permissions.tool.todowrite.description": "Ažuriraj listu zadataka",
"settings.permissions.tool.webfetch.title": "Web preuzimanje", "settings.permissions.tool.webfetch.title": "Web preuzimanje",
"settings.permissions.tool.webfetch.description": "Preuzmi sadržaj sa URL-a", "settings.permissions.tool.webfetch.description": "Preuzmi sadržaj sa URL-a",
"settings.permissions.tool.websearch.title": "Web pretraga", "settings.permissions.tool.websearch.title": "Web pretraga",
@ -1020,7 +1015,6 @@ export const dict = {
"session.review.noVcs.createGit.description": "Pratite, pregledajte i poništite promjene u ovom projektu", "session.review.noVcs.createGit.description": "Pratite, pregledajte i poništite promjene u ovom projektu",
"session.review.noVcs.createGit.actionLoading": "Kreiranje Git repozitorija...", "session.review.noVcs.createGit.actionLoading": "Kreiranje Git repozitorija...",
"session.review.noVcs.createGit.action": "Kreiraj Git repozitorij", "session.review.noVcs.createGit.action": "Kreiraj Git repozitorij",
"session.todo.progress": "{{done}} od {{total}} zadataka završeno",
"session.question.progress": "{{current}} od {{total}} pitanja", "session.question.progress": "{{current}} od {{total}} pitanja",
"session.header.open.finder": "Finder", "session.header.open.finder": "Finder",
"session.header.open.fileExplorer": "File Explorer", "session.header.open.fileExplorer": "File Explorer",

View file

@ -632,9 +632,6 @@ export const dict = {
"session.messages.jumpToLatest": "Gå til seneste", "session.messages.jumpToLatest": "Gå til seneste",
"session.context.addToContext": "Tilføj {{selection}} til kontekst", "session.context.addToContext": "Tilføj {{selection}} til kontekst",
"session.todo.title": "Opgaver",
"session.todo.collapse": "Skjul",
"session.todo.expand": "Udvid",
"session.question.minimize": "Minimer spørgsmål", "session.question.minimize": "Minimer spørgsmål",
"session.question.restore": "Gendan spørgsmål", "session.question.restore": "Gendan spørgsmål",
"session.question.pending.one": "{{count}} afventende spørgsmål", "session.question.pending.one": "{{count}} afventende spørgsmål",
@ -946,8 +943,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "Indlæs en færdighed efter navn", "settings.permissions.tool.skill.description": "Indlæs en færdighed efter navn",
"settings.permissions.tool.lsp.title": "LSP", "settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "Kør sprogserverforespørgsler", "settings.permissions.tool.lsp.description": "Kør sprogserverforespørgsler",
"settings.permissions.tool.todowrite.title": "Skriv To-do",
"settings.permissions.tool.todowrite.description": "Opdater to-do listen",
"settings.permissions.tool.webfetch.title": "Webhentning", "settings.permissions.tool.webfetch.title": "Webhentning",
"settings.permissions.tool.webfetch.description": "Hent indhold fra en URL", "settings.permissions.tool.webfetch.description": "Hent indhold fra en URL",
"settings.permissions.tool.websearch.title": "Websøgning", "settings.permissions.tool.websearch.title": "Websøgning",
@ -1012,7 +1007,6 @@ export const dict = {
"session.review.noVcs.createGit.description": "Spor, gennemgå og fortryd ændringer i dette projekt", "session.review.noVcs.createGit.description": "Spor, gennemgå og fortryd ændringer i dette projekt",
"session.review.noVcs.createGit.actionLoading": "Opretter Git-repository...", "session.review.noVcs.createGit.actionLoading": "Opretter Git-repository...",
"session.review.noVcs.createGit.action": "Opret Git-repository", "session.review.noVcs.createGit.action": "Opret Git-repository",
"session.todo.progress": "{{done}} af {{total}} opgaver fuldført",
"session.question.progress": "{{current}} af {{total}} spørgsmål", "session.question.progress": "{{current}} af {{total}} spørgsmål",
"session.header.open.finder": "Finder", "session.header.open.finder": "Finder",
"session.header.open.fileExplorer": "Stifinder", "session.header.open.fileExplorer": "Stifinder",

View file

@ -591,9 +591,6 @@ export const dict = {
"session.messages.loading": "Lade Nachrichten...", "session.messages.loading": "Lade Nachrichten...",
"session.messages.jumpToLatest": "Zum neuesten springen", "session.messages.jumpToLatest": "Zum neuesten springen",
"session.context.addToContext": "{{selection}} zum Kontext hinzufügen", "session.context.addToContext": "{{selection}} zum Kontext hinzufügen",
"session.todo.title": "Aufgaben",
"session.todo.collapse": "Einklappen",
"session.todo.expand": "Ausklappen",
"session.question.minimize": "Frage minimieren", "session.question.minimize": "Frage minimieren",
"session.question.restore": "Frage wiederherstellen", "session.question.restore": "Frage wiederherstellen",
"session.question.pending.one": "{{count}} ausstehende Frage", "session.question.pending.one": "{{count}} ausstehende Frage",
@ -892,8 +889,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "Eine Fähigkeit nach Namen laden", "settings.permissions.tool.skill.description": "Eine Fähigkeit nach Namen laden",
"settings.permissions.tool.lsp.title": "LSP", "settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "Language-Server-Abfragen ausführen", "settings.permissions.tool.lsp.description": "Language-Server-Abfragen ausführen",
"settings.permissions.tool.todowrite.title": "Todo schreiben",
"settings.permissions.tool.todowrite.description": "Die Todo-Liste aktualisieren",
"settings.permissions.tool.webfetch.title": "Web-Abruf", "settings.permissions.tool.webfetch.title": "Web-Abruf",
"settings.permissions.tool.webfetch.description": "Inhalt von einer URL abrufen", "settings.permissions.tool.webfetch.description": "Inhalt von einer URL abrufen",
"settings.permissions.tool.websearch.title": "Web-Suche", "settings.permissions.tool.websearch.title": "Web-Suche",
@ -957,7 +952,6 @@ export const dict = {
"Änderungen in diesem Projekt verfolgen, überprüfen und rückgängig machen", "Änderungen in diesem Projekt verfolgen, überprüfen und rückgängig machen",
"session.review.noVcs.createGit.actionLoading": "Git-Repository wird erstellt...", "session.review.noVcs.createGit.actionLoading": "Git-Repository wird erstellt...",
"session.review.noVcs.createGit.action": "Git-Repository erstellen", "session.review.noVcs.createGit.action": "Git-Repository erstellen",
"session.todo.progress": "{{done}} von {{total}} Aufgaben erledigt",
"session.question.progress": "{{current}} von {{total}} Fragen", "session.question.progress": "{{current}} von {{total}} Fragen",
"session.header.open.finder": "Finder", "session.header.open.finder": "Finder",
"session.header.open.fileExplorer": "Datei-Explorer", "session.header.open.fileExplorer": "Datei-Explorer",

View file

@ -659,10 +659,6 @@ export const dict = {
"session.messages.jumpToLatest": "Jump to latest", "session.messages.jumpToLatest": "Jump to latest",
"session.context.addToContext": "Add {{selection}} to context", "session.context.addToContext": "Add {{selection}} to context",
"session.todo.title": "Todos",
"session.todo.collapse": "Collapse",
"session.todo.expand": "Expand",
"session.todo.progress": "{{done}} of {{total}} todos completed",
"session.question.progress": "{{current}} of {{total}} questions", "session.question.progress": "{{current}} of {{total}} questions",
"session.question.minimize": "Minimize question", "session.question.minimize": "Minimize question",
"session.question.restore": "Restore question", "session.question.restore": "Restore question",
@ -1040,8 +1036,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "Load a skill by name", "settings.permissions.tool.skill.description": "Load a skill by name",
"settings.permissions.tool.lsp.title": "LSP", "settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "Run language server queries", "settings.permissions.tool.lsp.description": "Run language server queries",
"settings.permissions.tool.todowrite.title": "Todo Write",
"settings.permissions.tool.todowrite.description": "Update the todo list",
"settings.permissions.tool.webfetch.title": "Web Fetch", "settings.permissions.tool.webfetch.title": "Web Fetch",
"settings.permissions.tool.webfetch.description": "Fetch content from a URL", "settings.permissions.tool.webfetch.description": "Fetch content from a URL",
"settings.permissions.tool.websearch.title": "Web Search", "settings.permissions.tool.websearch.title": "Web Search",

View file

@ -638,9 +638,6 @@ export const dict = {
"session.messages.jumpToLatest": "Ir al último", "session.messages.jumpToLatest": "Ir al último",
"session.context.addToContext": "Añadir {{selection}} al contexto", "session.context.addToContext": "Añadir {{selection}} al contexto",
"session.todo.title": "Tareas",
"session.todo.collapse": "Contraer",
"session.todo.expand": "Expandir",
"session.question.minimize": "Minimizar pregunta", "session.question.minimize": "Minimizar pregunta",
"session.question.restore": "Restaurar pregunta", "session.question.restore": "Restaurar pregunta",
"session.question.pending.one": "{{count}} pregunta pendiente", "session.question.pending.one": "{{count}} pregunta pendiente",
@ -962,8 +959,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "Cargar una habilidad por nombre", "settings.permissions.tool.skill.description": "Cargar una habilidad por nombre",
"settings.permissions.tool.lsp.title": "LSP", "settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "Ejecutar consultas de servidor de lenguaje", "settings.permissions.tool.lsp.description": "Ejecutar consultas de servidor de lenguaje",
"settings.permissions.tool.todowrite.title": "Escribir Todo",
"settings.permissions.tool.todowrite.description": "Actualizar la lista de tareas",
"settings.permissions.tool.webfetch.title": "Web Fetch", "settings.permissions.tool.webfetch.title": "Web Fetch",
"settings.permissions.tool.webfetch.description": "Obtener contenido de una URL", "settings.permissions.tool.webfetch.description": "Obtener contenido de una URL",
"settings.permissions.tool.websearch.title": "Búsqueda Web", "settings.permissions.tool.websearch.title": "Búsqueda Web",
@ -1028,7 +1023,6 @@ export const dict = {
"session.review.noVcs.createGit.description": "Rastrea, revisa y deshaz cambios en este proyecto", "session.review.noVcs.createGit.description": "Rastrea, revisa y deshaz cambios en este proyecto",
"session.review.noVcs.createGit.actionLoading": "Creando repositorio Git...", "session.review.noVcs.createGit.actionLoading": "Creando repositorio Git...",
"session.review.noVcs.createGit.action": "Crear repositorio Git", "session.review.noVcs.createGit.action": "Crear repositorio Git",
"session.todo.progress": "{{done}} de {{total}} tareas completadas",
"session.question.progress": "{{current}} de {{total}} preguntas", "session.question.progress": "{{current}} de {{total}} preguntas",
"session.header.open.finder": "Finder", "session.header.open.finder": "Finder",
"session.header.open.fileExplorer": "Explorador de archivos", "session.header.open.fileExplorer": "Explorador de archivos",

View file

@ -587,9 +587,6 @@ export const dict = {
"session.messages.loading": "Chargement des messages...", "session.messages.loading": "Chargement des messages...",
"session.messages.jumpToLatest": "Aller au dernier", "session.messages.jumpToLatest": "Aller au dernier",
"session.context.addToContext": "Ajouter {{selection}} au contexte", "session.context.addToContext": "Ajouter {{selection}} au contexte",
"session.todo.title": "Tâches",
"session.todo.collapse": "Réduire",
"session.todo.expand": "Développer",
"session.question.minimize": "Réduire la question", "session.question.minimize": "Réduire la question",
"session.question.restore": "Restaurer la question", "session.question.restore": "Restaurer la question",
"session.question.pending.one": "{{count}} question en attente", "session.question.pending.one": "{{count}} question en attente",
@ -891,8 +888,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "Charger une compétence par son nom", "settings.permissions.tool.skill.description": "Charger une compétence par son nom",
"settings.permissions.tool.lsp.title": "LSP", "settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "Exécuter des requêtes de serveur de langage", "settings.permissions.tool.lsp.description": "Exécuter des requêtes de serveur de langage",
"settings.permissions.tool.todowrite.title": "Écrire Todo",
"settings.permissions.tool.todowrite.description": "Mettre à jour la liste de tâches",
"settings.permissions.tool.webfetch.title": "Récupération Web", "settings.permissions.tool.webfetch.title": "Récupération Web",
"settings.permissions.tool.webfetch.description": "Récupérer le contenu d'une URL", "settings.permissions.tool.webfetch.description": "Récupérer le contenu d'une URL",
"settings.permissions.tool.websearch.title": "Recherche Web", "settings.permissions.tool.websearch.title": "Recherche Web",
@ -955,7 +950,6 @@ export const dict = {
"session.review.noVcs.createGit.description": "Suivre, examiner et annuler les modifications dans ce projet", "session.review.noVcs.createGit.description": "Suivre, examiner et annuler les modifications dans ce projet",
"session.review.noVcs.createGit.actionLoading": "Création du dépôt Git...", "session.review.noVcs.createGit.actionLoading": "Création du dépôt Git...",
"session.review.noVcs.createGit.action": "Créer un dépôt Git", "session.review.noVcs.createGit.action": "Créer un dépôt Git",
"session.todo.progress": "{{done}} tâches sur {{total}} terminées",
"session.question.progress": "{{current}} questions sur {{total}}", "session.question.progress": "{{current}} questions sur {{total}}",
"session.header.open.finder": "Finder", "session.header.open.finder": "Finder",
"session.header.open.fileExplorer": "Explorateur de fichiers", "session.header.open.fileExplorer": "Explorateur de fichiers",

View file

@ -579,9 +579,6 @@ export const dict = {
"session.messages.loading": "メッセージを読み込み中...", "session.messages.loading": "メッセージを読み込み中...",
"session.messages.jumpToLatest": "最新へジャンプ", "session.messages.jumpToLatest": "最新へジャンプ",
"session.context.addToContext": "{{selection}}をコンテキストに追加", "session.context.addToContext": "{{selection}}をコンテキストに追加",
"session.todo.title": "ToDo",
"session.todo.collapse": "折りたたむ",
"session.todo.expand": "展開",
"session.question.minimize": "質問を最小化", "session.question.minimize": "質問を最小化",
"session.question.restore": "質問を復元", "session.question.restore": "質問を復元",
"session.question.pending.one": "{{count}}件の保留中の質問", "session.question.pending.one": "{{count}}件の保留中の質問",
@ -874,8 +871,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "名前によるスキルの読み込み", "settings.permissions.tool.skill.description": "名前によるスキルの読み込み",
"settings.permissions.tool.lsp.title": "LSP", "settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "言語サーバークエリの実行", "settings.permissions.tool.lsp.description": "言語サーバークエリの実行",
"settings.permissions.tool.todowrite.title": "Todo書き込み",
"settings.permissions.tool.todowrite.description": "Todoリストの更新",
"settings.permissions.tool.webfetch.title": "Web取得", "settings.permissions.tool.webfetch.title": "Web取得",
"settings.permissions.tool.webfetch.description": "URLからコンテンツを取得", "settings.permissions.tool.webfetch.description": "URLからコンテンツを取得",
"settings.permissions.tool.websearch.title": "Web検索", "settings.permissions.tool.websearch.title": "Web検索",
@ -938,7 +933,6 @@ export const dict = {
"session.review.noVcs.createGit.description": "このプロジェクトの変更を追跡、レビュー、元に戻す", "session.review.noVcs.createGit.description": "このプロジェクトの変更を追跡、レビュー、元に戻す",
"session.review.noVcs.createGit.actionLoading": "Git リポジトリを作成中...", "session.review.noVcs.createGit.actionLoading": "Git リポジトリを作成中...",
"session.review.noVcs.createGit.action": "Git リポジトリを作成", "session.review.noVcs.createGit.action": "Git リポジトリを作成",
"session.todo.progress": "{{done}} 個中 {{total}} 個の Todo が完了",
"session.question.progress": "{{total}} 問中 {{current}} 問", "session.question.progress": "{{total}} 問中 {{current}} 問",
"session.header.open.finder": "Finder", "session.header.open.finder": "Finder",
"session.header.open.fileExplorer": "エクスプローラー", "session.header.open.fileExplorer": "エクスプローラー",

View file

@ -471,9 +471,6 @@ export const dict = {
"session.messages.loading": "메시지 로드 중...", "session.messages.loading": "메시지 로드 중...",
"session.messages.jumpToLatest": "최신으로 이동", "session.messages.jumpToLatest": "최신으로 이동",
"session.context.addToContext": "컨텍스트에 {{selection}} 추가", "session.context.addToContext": "컨텍스트에 {{selection}} 추가",
"session.todo.title": "할 일",
"session.todo.collapse": "접기",
"session.todo.expand": "펼치기",
"session.followupDock.summary.one": "{{count}}개의 대기 중인 메시지", "session.followupDock.summary.one": "{{count}}개의 대기 중인 메시지",
"session.followupDock.summary.other": "{{count}}개의 대기 중인 메시지", "session.followupDock.summary.other": "{{count}}개의 대기 중인 메시지",
"session.followupDock.sendNow": "지금 전송", "session.followupDock.sendNow": "지금 전송",
@ -721,8 +718,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "이름으로 기술 로드", "settings.permissions.tool.skill.description": "이름으로 기술 로드",
"settings.permissions.tool.lsp.title": "LSP", "settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "언어 서버 쿼리 실행", "settings.permissions.tool.lsp.description": "언어 서버 쿼리 실행",
"settings.permissions.tool.todowrite.title": "할 일 쓰기",
"settings.permissions.tool.todowrite.description": "할 일 목록 업데이트",
"settings.permissions.tool.webfetch.title": "웹 가져오기", "settings.permissions.tool.webfetch.title": "웹 가져오기",
"settings.permissions.tool.webfetch.description": "URL에서 콘텐츠 가져오기", "settings.permissions.tool.webfetch.description": "URL에서 콘텐츠 가져오기",
"settings.permissions.tool.websearch.title": "웹 검색", "settings.permissions.tool.websearch.title": "웹 검색",
@ -785,7 +780,6 @@ export const dict = {
"session.review.noVcs.createGit.description": "이 프로젝트의 변경 사항을 추적, 검토 및 실행 취소", "session.review.noVcs.createGit.description": "이 프로젝트의 변경 사항을 추적, 검토 및 실행 취소",
"session.review.noVcs.createGit.actionLoading": "Git 저장소 생성 중...", "session.review.noVcs.createGit.actionLoading": "Git 저장소 생성 중...",
"session.review.noVcs.createGit.action": "Git 저장소 생성", "session.review.noVcs.createGit.action": "Git 저장소 생성",
"session.todo.progress": "{{total}}개의 할 일 중 {{done}}개 완료",
"session.question.progress": "{{total}}개의 질문 중 {{current}}개", "session.question.progress": "{{total}}개의 질문 중 {{current}}개",
"session.header.open.finder": "Finder", "session.header.open.finder": "Finder",
"session.header.open.fileExplorer": "파일 탐색기", "session.header.open.fileExplorer": "파일 탐색기",

View file

@ -532,9 +532,6 @@ export const dict = {
"session.messages.jumpToLatest": "Hopp til nyeste", "session.messages.jumpToLatest": "Hopp til nyeste",
"session.context.addToContext": "Legg til {{selection}} i kontekst", "session.context.addToContext": "Legg til {{selection}} i kontekst",
"session.todo.title": "Oppgaver",
"session.todo.collapse": "Skjul",
"session.todo.expand": "Utvid",
"session.followupDock.summary.one": "{{count}} melding i kø", "session.followupDock.summary.one": "{{count}} melding i kø",
"session.followupDock.summary.other": "{{count}} meldinger i kø", "session.followupDock.summary.other": "{{count}} meldinger i kø",
"session.followupDock.sendNow": "Send nå", "session.followupDock.sendNow": "Send nå",
@ -806,8 +803,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "Last en ferdighet etter navn", "settings.permissions.tool.skill.description": "Last en ferdighet etter navn",
"settings.permissions.tool.lsp.title": "LSP", "settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "Kjør språkserverforespørsler", "settings.permissions.tool.lsp.description": "Kjør språkserverforespørsler",
"settings.permissions.tool.todowrite.title": "Skriv gjøremål",
"settings.permissions.tool.todowrite.description": "Oppdater gjøremålslisten",
"settings.permissions.tool.webfetch.title": "Webhenting", "settings.permissions.tool.webfetch.title": "Webhenting",
"settings.permissions.tool.webfetch.description": "Hent innhold fra en URL", "settings.permissions.tool.webfetch.description": "Hent innhold fra en URL",
"settings.permissions.tool.websearch.title": "Websøk", "settings.permissions.tool.websearch.title": "Websøk",
@ -872,7 +867,6 @@ export const dict = {
"session.review.noVcs.createGit.description": "Spor, gjennomgå og angre endringer i dette prosjektet", "session.review.noVcs.createGit.description": "Spor, gjennomgå og angre endringer i dette prosjektet",
"session.review.noVcs.createGit.actionLoading": "Oppretter Git-depot...", "session.review.noVcs.createGit.actionLoading": "Oppretter Git-depot...",
"session.review.noVcs.createGit.action": "Opprett Git-depot", "session.review.noVcs.createGit.action": "Opprett Git-depot",
"session.todo.progress": "{{done}} av {{total}} oppgaver fullført",
"session.question.progress": "{{current}} av {{total}} spørsmål", "session.question.progress": "{{current}} av {{total}} spørsmål",
"session.header.open.finder": "Finder", "session.header.open.finder": "Finder",
"session.header.open.fileExplorer": "Filutforsker", "session.header.open.fileExplorer": "Filutforsker",

View file

@ -582,9 +582,6 @@ export const dict = {
"session.messages.loading": "Ładowanie wiadomości...", "session.messages.loading": "Ładowanie wiadomości...",
"session.messages.jumpToLatest": "Przejdź do najnowszych", "session.messages.jumpToLatest": "Przejdź do najnowszych",
"session.context.addToContext": "Dodaj {{selection}} do kontekstu", "session.context.addToContext": "Dodaj {{selection}} do kontekstu",
"session.todo.title": "Zadania",
"session.todo.collapse": "Zwiń",
"session.todo.expand": "Rozwiń",
"session.question.minimize": "Zminimalizuj pytanie", "session.question.minimize": "Zminimalizuj pytanie",
"session.question.restore": "Przywróć pytanie", "session.question.restore": "Przywróć pytanie",
"session.question.pending.one": "{{count}} oczekujące pytanie", "session.question.pending.one": "{{count}} oczekujące pytanie",
@ -879,8 +876,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "Ładowanie umiejętności według nazwy", "settings.permissions.tool.skill.description": "Ładowanie umiejętności według nazwy",
"settings.permissions.tool.lsp.title": "LSP", "settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "Uruchamianie zapytań serwera językowego", "settings.permissions.tool.lsp.description": "Uruchamianie zapytań serwera językowego",
"settings.permissions.tool.todowrite.title": "Zapis Todo",
"settings.permissions.tool.todowrite.description": "Aktualizacja listy zadań",
"settings.permissions.tool.webfetch.title": "Pobieranie z sieci", "settings.permissions.tool.webfetch.title": "Pobieranie z sieci",
"settings.permissions.tool.webfetch.description": "Pobieranie zawartości z adresu URL", "settings.permissions.tool.webfetch.description": "Pobieranie zawartości z adresu URL",
"settings.permissions.tool.websearch.title": "Wyszukiwanie w sieci", "settings.permissions.tool.websearch.title": "Wyszukiwanie w sieci",
@ -943,7 +938,6 @@ export const dict = {
"session.review.noVcs.createGit.description": "Śledź, przeglądaj i cofaj zmiany w tym projekcie", "session.review.noVcs.createGit.description": "Śledź, przeglądaj i cofaj zmiany w tym projekcie",
"session.review.noVcs.createGit.actionLoading": "Tworzenie repozytorium Git...", "session.review.noVcs.createGit.actionLoading": "Tworzenie repozytorium Git...",
"session.review.noVcs.createGit.action": "Utwórz repozytorium Git", "session.review.noVcs.createGit.action": "Utwórz repozytorium Git",
"session.todo.progress": "Ukończono {{done}} z {{total}} zadań",
"session.question.progress": "{{current}} z {{total}} pytań", "session.question.progress": "{{current}} z {{total}} pytań",
"session.header.open.finder": "Finder", "session.header.open.finder": "Finder",
"session.header.open.fileExplorer": "Eksplorator plików", "session.header.open.fileExplorer": "Eksplorator plików",

View file

@ -635,9 +635,6 @@ export const dict = {
"session.messages.jumpToLatest": "Перейти к последнему", "session.messages.jumpToLatest": "Перейти к последнему",
"session.context.addToContext": "Добавить {{selection}} в контекст", "session.context.addToContext": "Добавить {{selection}} в контекст",
"session.todo.title": "Задачи",
"session.todo.collapse": "Свернуть",
"session.todo.expand": "Развернуть",
"session.question.minimize": "Свернуть вопрос", "session.question.minimize": "Свернуть вопрос",
"session.question.restore": "Восстановить вопрос", "session.question.restore": "Восстановить вопрос",
"session.question.pending.one": "{{count}} вопрос без ответа", "session.question.pending.one": "{{count}} вопрос без ответа",
@ -956,8 +953,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "Загрузка навыка по имени", "settings.permissions.tool.skill.description": "Загрузка навыка по имени",
"settings.permissions.tool.lsp.title": "LSP", "settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "Запросы к языковому серверу", "settings.permissions.tool.lsp.description": "Запросы к языковому серверу",
"settings.permissions.tool.todowrite.title": "Todo Write",
"settings.permissions.tool.todowrite.description": "Обновление списка задач",
"settings.permissions.tool.webfetch.title": "Web Fetch", "settings.permissions.tool.webfetch.title": "Web Fetch",
"settings.permissions.tool.webfetch.description": "Получение контента по URL", "settings.permissions.tool.webfetch.description": "Получение контента по URL",
"settings.permissions.tool.websearch.title": "Web Search", "settings.permissions.tool.websearch.title": "Web Search",
@ -1023,7 +1018,6 @@ export const dict = {
"session.review.noVcs.createGit.description": "Отслеживайте, просматривайте и отменяйте изменения в этом проекте", "session.review.noVcs.createGit.description": "Отслеживайте, просматривайте и отменяйте изменения в этом проекте",
"session.review.noVcs.createGit.actionLoading": "Создание репозитория Git...", "session.review.noVcs.createGit.actionLoading": "Создание репозитория Git...",
"session.review.noVcs.createGit.action": "Создать репозиторий Git", "session.review.noVcs.createGit.action": "Создать репозиторий Git",
"session.todo.progress": "Выполнено {{done}} из {{total}} задач",
"session.question.progress": "{{current}} из {{total}} вопросов", "session.question.progress": "{{current}} из {{total}} вопросов",
"session.header.open.finder": "Finder", "session.header.open.finder": "Finder",
"session.header.open.fileExplorer": "Проводник", "session.header.open.fileExplorer": "Проводник",

View file

@ -632,9 +632,6 @@ export const dict = {
"session.messages.jumpToLatest": "ไปที่ล่าสุด", "session.messages.jumpToLatest": "ไปที่ล่าสุด",
"session.context.addToContext": "เพิ่ม {{selection}} ไปยังบริบท", "session.context.addToContext": "เพิ่ม {{selection}} ไปยังบริบท",
"session.todo.title": "สิ่งที่ต้องทำ",
"session.todo.collapse": "ย่อ",
"session.todo.expand": "ขยาย",
"session.question.minimize": "ย่อคำถาม", "session.question.minimize": "ย่อคำถาม",
"session.question.restore": "คืนค่าคำถาม", "session.question.restore": "คืนค่าคำถาม",
"session.question.pending.one": "คำถามที่รอดำเนินการ {{count}} ข้อ", "session.question.pending.one": "คำถามที่รอดำเนินการ {{count}} ข้อ",
@ -942,8 +939,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "โหลดทักษะตามชื่อ", "settings.permissions.tool.skill.description": "โหลดทักษะตามชื่อ",
"settings.permissions.tool.lsp.title": "LSP", "settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "เรียกใช้การสืบค้นเซิร์ฟเวอร์ภาษา", "settings.permissions.tool.lsp.description": "เรียกใช้การสืบค้นเซิร์ฟเวอร์ภาษา",
"settings.permissions.tool.todowrite.title": "เขียนรายการงาน",
"settings.permissions.tool.todowrite.description": "อัปเดตรายการงาน",
"settings.permissions.tool.webfetch.title": "ดึงข้อมูลจากเว็บ", "settings.permissions.tool.webfetch.title": "ดึงข้อมูลจากเว็บ",
"settings.permissions.tool.webfetch.description": "ดึงเนื้อหาจาก URL", "settings.permissions.tool.webfetch.description": "ดึงเนื้อหาจาก URL",
"settings.permissions.tool.websearch.title": "ค้นหาเว็บ", "settings.permissions.tool.websearch.title": "ค้นหาเว็บ",
@ -1008,7 +1003,6 @@ export const dict = {
"session.review.noVcs.createGit.description": "ติดตาม ตรวจสอบ และเลิกทำสิ่งเปลี่ยนแปลงในโปรเจกต์นี้", "session.review.noVcs.createGit.description": "ติดตาม ตรวจสอบ และเลิกทำสิ่งเปลี่ยนแปลงในโปรเจกต์นี้",
"session.review.noVcs.createGit.actionLoading": "กำลังสร้าง Git รีโพซิทอรี...", "session.review.noVcs.createGit.actionLoading": "กำลังสร้าง Git รีโพซิทอรี...",
"session.review.noVcs.createGit.action": "สร้าง Git รีโพซิทอรี", "session.review.noVcs.createGit.action": "สร้าง Git รีโพซิทอรี",
"session.todo.progress": "เสร็จสิ้น {{done}} จาก {{total}} รายการ",
"session.question.progress": "{{current}} จาก {{total}} คำถาม", "session.question.progress": "{{current}} จาก {{total}} คำถาม",
"session.header.open.finder": "Finder", "session.header.open.finder": "Finder",
"session.header.open.fileExplorer": "File Explorer", "session.header.open.fileExplorer": "File Explorer",

View file

@ -642,9 +642,6 @@ export const dict = {
"session.messages.jumpToLatest": "En sona atla", "session.messages.jumpToLatest": "En sona atla",
"session.context.addToContext": "{{selection}} bağlama ekle", "session.context.addToContext": "{{selection}} bağlama ekle",
"session.todo.title": "Görevler",
"session.todo.collapse": "Daralt",
"session.todo.expand": "Genişlet",
"session.question.minimize": "Soruyu küçült", "session.question.minimize": "Soruyu küçült",
"session.question.restore": "Soruyu geri yükle", "session.question.restore": "Soruyu geri yükle",
"session.question.pending.one": "{{count}} bekleyen soru", "session.question.pending.one": "{{count}} bekleyen soru",
@ -962,8 +959,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "Ada göre bir beceri yükle", "settings.permissions.tool.skill.description": "Ada göre bir beceri yükle",
"settings.permissions.tool.lsp.title": "LSP", "settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "Dil sunucusu sorguları çalıştır", "settings.permissions.tool.lsp.description": "Dil sunucusu sorguları çalıştır",
"settings.permissions.tool.todowrite.title": "Görev Yaz",
"settings.permissions.tool.todowrite.description": "Görev listesini güncelle",
"settings.permissions.tool.webfetch.title": "Web Getir", "settings.permissions.tool.webfetch.title": "Web Getir",
"settings.permissions.tool.webfetch.description": "Bir URL'den içerik getir", "settings.permissions.tool.webfetch.description": "Bir URL'den içerik getir",
"settings.permissions.tool.websearch.title": "Web Ara", "settings.permissions.tool.websearch.title": "Web Ara",
@ -1028,7 +1023,6 @@ export const dict = {
"session.review.noVcs.createGit.description": "Bu projedeki değişiklikleri takip et, incele ve geri al", "session.review.noVcs.createGit.description": "Bu projedeki değişiklikleri takip et, incele ve geri al",
"session.review.noVcs.createGit.actionLoading": "Git deposu oluşturuluyor...", "session.review.noVcs.createGit.actionLoading": "Git deposu oluşturuluyor...",
"session.review.noVcs.createGit.action": "Git deposu oluştur", "session.review.noVcs.createGit.action": "Git deposu oluştur",
"session.todo.progress": "{{total}} görevin {{done}} tanesi tamamlandı",
"session.question.progress": "{{total}} sorunun {{current}} tanesi", "session.question.progress": "{{total}} sorunun {{current}} tanesi",
"session.header.open.finder": "Finder", "session.header.open.finder": "Finder",
"session.header.open.fileExplorer": "Dosya Gezgini", "session.header.open.fileExplorer": "Dosya Gezgini",

View file

@ -663,10 +663,6 @@ export const dict = {
"session.messages.jumpToLatest": "Перейти до останніх", "session.messages.jumpToLatest": "Перейти до останніх",
"session.context.addToContext": "Додати {{selection}} до контексту", "session.context.addToContext": "Додати {{selection}} до контексту",
"session.todo.title": "Завдання",
"session.todo.collapse": "Згорнути",
"session.todo.expand": "Розгорнути",
"session.todo.progress": "Виконано {{done}} з {{total}} завдань",
"session.question.progress": "{{current}} з {{total}} запитань", "session.question.progress": "{{current}} з {{total}} запитань",
"session.question.minimize": "Згорнути запитання", "session.question.minimize": "Згорнути запитання",
"session.question.restore": "Відновити запитання", "session.question.restore": "Відновити запитання",
@ -1047,8 +1043,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "Завантаження навички за назвою", "settings.permissions.tool.skill.description": "Завантаження навички за назвою",
"settings.permissions.tool.lsp.title": "LSP", "settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "Виконання запитів мовного сервера", "settings.permissions.tool.lsp.description": "Виконання запитів мовного сервера",
"settings.permissions.tool.todowrite.title": "Todo Write",
"settings.permissions.tool.todowrite.description": "Оновлення списку завдань",
"settings.permissions.tool.webfetch.title": "Web Fetch", "settings.permissions.tool.webfetch.title": "Web Fetch",
"settings.permissions.tool.webfetch.description": "Отримання вмісту з URL", "settings.permissions.tool.webfetch.description": "Отримання вмісту з URL",
"settings.permissions.tool.websearch.title": "Web Search", "settings.permissions.tool.websearch.title": "Web Search",

View file

@ -630,9 +630,6 @@ export const dict = {
"session.messages.loading": "正在加载消息...", "session.messages.loading": "正在加载消息...",
"session.messages.jumpToLatest": "跳转到最新", "session.messages.jumpToLatest": "跳转到最新",
"session.context.addToContext": "将 {{selection}} 添加到上下文", "session.context.addToContext": "将 {{selection}} 添加到上下文",
"session.todo.title": "待办事项",
"session.todo.collapse": "折叠",
"session.todo.expand": "展开",
"session.question.minimize": "最小化问题", "session.question.minimize": "最小化问题",
"session.question.restore": "恢复问题", "session.question.restore": "恢复问题",
"session.question.pending.one": "{{count}} 个待处理问题", "session.question.pending.one": "{{count}} 个待处理问题",
@ -935,8 +932,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "按名称加载技能", "settings.permissions.tool.skill.description": "按名称加载技能",
"settings.permissions.tool.lsp.title": "LSP", "settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "运行语言服务器查询", "settings.permissions.tool.lsp.description": "运行语言服务器查询",
"settings.permissions.tool.todowrite.title": "更新待办",
"settings.permissions.tool.todowrite.description": "更新待办列表",
"settings.permissions.tool.webfetch.title": "网页获取", "settings.permissions.tool.webfetch.title": "网页获取",
"settings.permissions.tool.webfetch.description": "从 URL 获取内容", "settings.permissions.tool.webfetch.description": "从 URL 获取内容",
"settings.permissions.tool.websearch.title": "网页搜索", "settings.permissions.tool.websearch.title": "网页搜索",
@ -1001,7 +996,6 @@ export const dict = {
"session.review.noVcs.createGit.description": "在此项目中跟踪、审查和撤消更改", "session.review.noVcs.createGit.description": "在此项目中跟踪、审查和撤消更改",
"session.review.noVcs.createGit.actionLoading": "正在创建 Git 仓库...", "session.review.noVcs.createGit.actionLoading": "正在创建 Git 仓库...",
"session.review.noVcs.createGit.action": "创建 Git 仓库", "session.review.noVcs.createGit.action": "创建 Git 仓库",
"session.todo.progress": "已完成 {{done}} 个任务(共 {{total}} 个)",
"session.question.progress": "{{current}}/{{total}} 个问题", "session.question.progress": "{{current}}/{{total}} 个问题",
"session.header.open.finder": "访达", "session.header.open.finder": "访达",
"session.header.open.fileExplorer": "文件资源管理器", "session.header.open.fileExplorer": "文件资源管理器",

View file

@ -626,9 +626,6 @@ export const dict = {
"session.messages.jumpToLatest": "跳到最新", "session.messages.jumpToLatest": "跳到最新",
"session.context.addToContext": "將 {{selection}} 新增到上下文", "session.context.addToContext": "將 {{selection}} 新增到上下文",
"session.todo.title": "待辦事項",
"session.todo.collapse": "折疊",
"session.todo.expand": "展開",
"session.question.minimize": "最小化問題", "session.question.minimize": "最小化問題",
"session.question.restore": "還原問題", "session.question.restore": "還原問題",
"session.question.pending.one": "{{count}} 個待處理問題", "session.question.pending.one": "{{count}} 個待處理問題",
@ -931,8 +928,6 @@ export const dict = {
"settings.permissions.tool.skill.description": "按名稱載入技能", "settings.permissions.tool.skill.description": "按名稱載入技能",
"settings.permissions.tool.lsp.title": "LSP", "settings.permissions.tool.lsp.title": "LSP",
"settings.permissions.tool.lsp.description": "執行語言伺服器查詢", "settings.permissions.tool.lsp.description": "執行語言伺服器查詢",
"settings.permissions.tool.todowrite.title": "更新待辦",
"settings.permissions.tool.todowrite.description": "更新待辦清單",
"settings.permissions.tool.webfetch.title": "Web Fetch", "settings.permissions.tool.webfetch.title": "Web Fetch",
"settings.permissions.tool.webfetch.description": "從 URL 取得內容", "settings.permissions.tool.webfetch.description": "從 URL 取得內容",
"settings.permissions.tool.websearch.title": "Web Search", "settings.permissions.tool.websearch.title": "Web Search",
@ -997,7 +992,6 @@ export const dict = {
"session.review.noVcs.createGit.description": "追蹤、檢閱及復原此專案中的變更", "session.review.noVcs.createGit.description": "追蹤、檢閱及復原此專案中的變更",
"session.review.noVcs.createGit.actionLoading": "正在建立 Git 儲存庫...", "session.review.noVcs.createGit.actionLoading": "正在建立 Git 儲存庫...",
"session.review.noVcs.createGit.action": "建立 Git 儲存庫", "session.review.noVcs.createGit.action": "建立 Git 儲存庫",
"session.todo.progress": "已完成 {{done}} 個待辦事項(共 {{total}} 個)",
"session.question.progress": "{{current}}/{{total}} 個問題", "session.question.progress": "{{current}}/{{total}} 個問題",
"session.header.open.finder": "Finder", "session.header.open.finder": "Finder",
"session.header.open.fileExplorer": "檔案總管", "session.header.open.fileExplorer": "檔案總管",

View file

@ -565,8 +565,6 @@ export default function Page() {
}) })
let reviewFrame: number | undefined let reviewFrame: number | undefined
let todoFrame: number | undefined
let todoTimer: number | undefined
let diffFrame: number | undefined let diffFrame: number | undefined
let diffTimer: number | undefined let diffTimer: number | undefined
@ -773,41 +771,6 @@ export default function Page() {
const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs
createEffect(
on(
() => {
const id = params.id
return [
sdk().directory,
id,
id ? (sync().data.session_status[id]?.type ?? "idle") : "idle",
id ? composer.blocked() : false,
] as const
},
([dir, id, status, blocked]) => {
if (todoFrame !== undefined) cancelAnimationFrame(todoFrame)
if (todoTimer !== undefined) window.clearTimeout(todoTimer)
todoFrame = undefined
todoTimer = undefined
if (!id) return
if (status === "idle" && !blocked) return
const cached = untrack(() => sync().data.todo[id] !== undefined)
todoFrame = requestAnimationFrame(() => {
todoFrame = undefined
todoTimer = window.setTimeout(() => {
todoTimer = undefined
if (sdk().directory !== dir || params.id !== id) return
untrack(() => {
void sync().session.todo(id, cached ? { force: true } : undefined)
})
}, 0)
})
},
{ defer: true },
),
)
createEffect( createEffect(
on( on(
() => visibleUserMessages().at(-1)?.id, () => visibleUserMessages().at(-1)?.id,
@ -1882,8 +1845,6 @@ export default function Page() {
onCleanup(() => { onCleanup(() => {
if (reviewFrame !== undefined) cancelAnimationFrame(reviewFrame) if (reviewFrame !== undefined) cancelAnimationFrame(reviewFrame)
if (todoFrame !== undefined) cancelAnimationFrame(todoFrame)
if (todoTimer !== undefined) window.clearTimeout(todoTimer)
if (diffFrame !== undefined) cancelAnimationFrame(diffFrame) if (diffFrame !== undefined) cancelAnimationFrame(diffFrame)
if (diffTimer !== undefined) window.clearTimeout(diffTimer) if (diffTimer !== undefined) window.clearTimeout(diffTimer)
if (scrollStateFrame !== undefined) cancelAnimationFrame(scrollStateFrame) if (scrollStateFrame !== undefined) cancelAnimationFrame(scrollStateFrame)
@ -1898,12 +1859,7 @@ export default function Page() {
sessionKey, sessionKey,
sessionID: () => params.id, sessionID: () => params.id,
prompt, prompt,
ready: () => !store.deferRender && messagesReady(),
centered, centered,
todo: {
collapsed: () => view().todoCollapsed.get(),
onToggle: () => view().todoCollapsed.set(!view().todoCollapsed.get()),
},
followup: () => followup: () =>
params.id && !isChildSession() params.id && !isChildSession()
? { ? {

View file

@ -1,7 +1,4 @@
import { createResizeObserver } from "@solid-primitives/resize-observer" import { type Accessor, createEffect, createMemo, createResource } from "solid-js"
import { useSpring } from "@opencode-ai/ui/motion-spring"
import { type Accessor, createEffect, createMemo, createResource, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import type { PromptInputState } from "@/components/prompt-input" import type { PromptInputState } from "@/components/prompt-input"
import { useSync } from "@/context/sync" import { useSync } from "@/context/sync"
import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff" import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff"
@ -26,12 +23,7 @@ export function createSessionComposerRegionController(input: {
sessionKey: Accessor<string> sessionKey: Accessor<string>
sessionID: Accessor<string | undefined> sessionID: Accessor<string | undefined>
prompt: PromptInputState prompt: PromptInputState
ready: Accessor<boolean>
centered: Accessor<boolean> centered: Accessor<boolean>
todo: {
collapsed: Accessor<boolean>
onToggle: () => void
}
followup: Accessor<SessionComposerFollowupDock | undefined> followup: Accessor<SessionComposerFollowupDock | undefined>
revert: Accessor<SessionComposerRevertDock | undefined> revert: Accessor<SessionComposerRevertDock | undefined>
onResponseSubmit: () => void onResponseSubmit: () => void
@ -40,41 +32,6 @@ export function createSessionComposerRegionController(input: {
setDockRef: (el: HTMLDivElement) => void setDockRef: (el: HTMLDivElement) => void
}) { }) {
const sync = useSync() const sync = useSync()
const [store, setStore] = createStore({
ready: input.ready() || input.state.dock(),
height: 320,
body: undefined as HTMLDivElement | undefined,
})
let timer: number | undefined
let frame: number | undefined
const clear = () => {
if (timer !== undefined) window.clearTimeout(timer)
if (frame !== undefined) cancelAnimationFrame(frame)
timer = undefined
frame = undefined
}
createEffect(() => {
input.sessionKey()
const ready = input.ready()
const dock = input.state.dock()
clear()
if (store.ready || (!ready && !dock)) return
if (dock) {
setStore("ready", true)
return
}
frame = requestAnimationFrame(() => {
frame = undefined
timer = window.setTimeout(() => {
setStore("ready", true)
timer = undefined
}, 140)
})
})
createEffect(() => { createEffect(() => {
if (!input.prompt.ready()) return if (!input.prompt.ready()) return
@ -92,27 +49,10 @@ export function createSessionComposerRegionController(input: {
}) })
}) })
createEffect(() => {
const el = store.body
if (!el) return
const update = () => setStore("height", el.getBoundingClientRect().height)
createResizeObserver(el, update)
update()
})
onCleanup(clear)
const parentID = createMemo(() => { const parentID = createMemo(() => {
const id = input.sessionID() const id = input.sessionID()
return id ? sync().session.get(id)?.parentID : undefined return id ? sync().session.get(id)?.parentID : undefined
}) })
const open = createMemo(() => store.ready && input.state.dock() && !input.state.closing())
const progress = useSpring(
() => (open() ? 1 : 0),
{ visualDuration: 0.3, bounce: 0 },
() => `${input.sessionKey()}\0${store.ready}`,
)
const value = createMemo(() => Math.max(0, Math.min(1, progress())))
const ready = Promise.resolve() const ready = Promise.resolve()
const [promptReady] = createResource( const [promptReady] = createResource(
() => input.prompt.ready.promise ?? ready, () => input.prompt.ready.promise ?? ready,
@ -122,7 +62,6 @@ export function createSessionComposerRegionController(input: {
return { return {
state: input.state, state: input.state,
centered: input.centered, centered: input.centered,
todo: input.todo,
followup: input.followup, followup: input.followup,
revert: input.revert, revert: input.revert,
onResponseSubmit: input.onResponseSubmit, onResponseSubmit: input.onResponseSubmit,
@ -134,11 +73,7 @@ export function createSessionComposerRegionController(input: {
showComposer: () => !input.state.blocked() || !!parentID(), showComposer: () => !input.state.blocked() || !!parentID(),
handoffPrompt: () => getSessionHandoff(input.sessionKey())?.prompt, handoffPrompt: () => getSessionHandoff(input.sessionKey())?.prompt,
promptReady: () => input.prompt.ready() || promptReady(), promptReady: () => input.prompt.ready() || promptReady(),
dock: () => (store.ready && input.state.dock()) || value() > 0.001, lift: () => (input.revert()?.items.length ? 18 : 0),
dockProgress: value,
dockHeight: () => Math.max(78, store.height),
lift: () => (input.revert()?.items.length ? 18 : 36 * value()),
setDockBodyRef: (el: HTMLDivElement) => setStore("body", el),
} }
} }

View file

@ -5,7 +5,6 @@ import { SessionPermissionDock } from "@/pages/session/composer/session-permissi
import { SessionQuestionDock } from "@/pages/session/composer/session-question-dock" import { SessionQuestionDock } from "@/pages/session/composer/session-question-dock"
import { SessionFollowupDock } from "@/pages/session/composer/session-followup-dock" import { SessionFollowupDock } from "@/pages/session/composer/session-followup-dock"
import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock" import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock"
import { SessionTodoDock } from "@/pages/session/composer/session-todo-dock"
import type { SessionComposerRegionController } from "./session-composer-region-controller" import type { SessionComposerRegionController } from "./session-composer-region-controller"
export function SessionComposerRegion(props: { export function SessionComposerRegion(props: {
@ -60,28 +59,6 @@ export function SessionComposerRegion(props: {
</Show> </Show>
<Show when={controller.showComposer()}> <Show when={controller.showComposer()}>
<Show when={controller.dock()}>
<div
classList={{
"overflow-hidden": true,
"pointer-events-none": controller.dockProgress() < 0.98,
}}
style={{
"max-height": `${controller.dockHeight() * controller.dockProgress()}px`,
}}
>
<div ref={controller.setDockBodyRef}>
<SessionTodoDock
todos={controller.state.todos()}
collapsed={controller.todo.collapsed()}
onToggle={controller.todo.onToggle}
collapseLabel={language.t("session.todo.collapse")}
expandLabel={language.t("session.todo.expand")}
dockProgress={controller.dockProgress()}
/>
</div>
</div>
</Show>
<Show <Show
when={controller.promptReady()} when={controller.promptReady()}
fallback={ fallback={
@ -98,10 +75,7 @@ export function SessionComposerRegion(props: {
</div> </div>
)} )}
</Show> </Show>
<div <div class="w-full min-h-32 md:min-h-40 rounded-md border border-border-weak-base bg-background-base/50 px-4 py-3 text-text-weak whitespace-pre-wrap pointer-events-none">
class="w-full min-h-32 md:min-h-40 rounded-md border border-border-weak-base bg-background-base/50 px-4 py-3 text-text-weak whitespace-pre-wrap pointer-events-none"
style={{ "margin-top": `${-36 * controller.dockProgress()}px` }}
>
{controller.handoffPrompt() || language.t("prompt.loading")} {controller.handoffPrompt() || language.t("prompt.loading")}
</div> </div>
</> </>
@ -109,11 +83,7 @@ export function SessionComposerRegion(props: {
> >
<Show when={rolled()} keyed> <Show when={rolled()} keyed>
{(revert) => ( {(revert) => (
<div <div>
style={{
"margin-top": `${-36 * controller.dockProgress()}px`,
}}
>
<SessionRevertDock <SessionRevertDock
items={revert.items} items={revert.items}
restoring={revert.restoring} restoring={revert.restoring}

View file

@ -1,6 +1,5 @@
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import type { PermissionRequest, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client" import type { PermissionRequest, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client"
import { todoDockAtBoundary, todoState } from "./session-composer-state"
import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree" import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
const session = (input: { id: string; parentID?: string }) => const session = (input: { id: string; parentID?: string }) =>
@ -104,35 +103,3 @@ describe("sessionQuestionRequest", () => {
expect(sessionQuestionRequest(sessions, questions, "root")?.id).toBe("q-grand") expect(sessionQuestionRequest(sessions, questions, "root")?.id).toBe("q-grand")
}) })
}) })
describe("todoState", () => {
test("hides when there are no todos", () => {
expect(todoState({ count: 0, done: false, live: true })).toBe("hide")
})
test("opens while the session is still working", () => {
expect(todoState({ count: 2, done: false, live: true })).toBe("open")
})
test("closes completed todos after a running turn", () => {
expect(todoState({ count: 2, done: true, live: true })).toBe("close")
})
test("clears stale todos when the turn ends", () => {
expect(todoState({ count: 2, done: false, live: false })).toBe("clear")
})
test("clears completed todos when the session is no longer live", () => {
expect(todoState({ count: 2, done: true, live: false })).toBe("clear")
})
})
describe("todoDockAtBoundary", () => {
test("shows active todos when entering a session", () => {
expect(todoDockAtBoundary("open")).toBe(true)
})
test("hides completed todos when entering a session", () => {
expect(todoDockAtBoundary("close")).toBe(false)
})
})

View file

@ -1,35 +1,20 @@
import { createEffect, createMemo, on, onCleanup } from "solid-js" import { createMemo } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2" import type { PermissionRequest, QuestionRequest } from "@opencode-ai/sdk/v2"
import { useParams } from "@solidjs/router" import { useParams } from "@solidjs/router"
import { showToast } from "@/utils/toast" import { showToast } from "@/utils/toast"
import { useServerSync } from "@/context/server-sync"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { usePermission } from "@/context/permission" import { usePermission } from "@/context/permission"
import { useSDK } from "@/context/sdk" import { useSDK } from "@/context/sdk"
import { useSync } from "@/context/sync" import { useSync } from "@/context/sync"
import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree" import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
export const todoState = (input: {
count: number
done: boolean
live: boolean
}): "hide" | "clear" | "open" | "close" => {
if (input.count === 0) return "hide"
if (!input.live) return "clear"
if (!input.done) return "open"
return "close"
}
export const todoDockAtBoundary = (state: ReturnType<typeof todoState>) => state === "open"
const idle = { type: "idle" as const } const idle = { type: "idle" as const }
export function createSessionComposerController(options?: { closeMs?: number | (() => number) }) { export function createSessionComposerController() {
const params = useParams() const params = useParams()
const sdk = useSDK() const sdk = useSDK()
const sync = useSync() const sync = useSync()
const serverSync = useServerSync()
const language = useLanguage() const language = useLanguage()
const permission = usePermission() const permission = usePermission()
@ -49,24 +34,8 @@ export function createSessionComposerController(options?: { closeMs?: number | (
return !!permissionRequest() || !!questionRequest() return !!permissionRequest() || !!questionRequest()
}) })
const todos = createMemo((): Todo[] => {
const id = params.id
if (!id) return []
return serverSync().session.data.todo[id] ?? []
})
const done = createMemo(
() => todos().length > 0 && todos().every((todo) => todo.status === "completed" || todo.status === "cancelled"),
)
const live = createMemo(() => sync().data.session_working(params.id ?? "") || blocked())
const [store, setStore] = createStore({ const [store, setStore] = createStore({
sessionID: params.id,
responding: undefined as string | undefined, responding: undefined as string | undefined,
dock: todos().length > 0 && !done() && live(),
closing: false,
opening: false,
}) })
const permissionResponding = createMemo(() => { const permissionResponding = createMemo(() => {
@ -92,112 +61,12 @@ export function createSessionComposerController(options?: { closeMs?: number | (
}) })
} }
let timer: number | undefined
let raf: number | undefined
const closeMs = () => {
const value = options?.closeMs
if (typeof value === "function") return Math.max(0, value())
if (typeof value === "number") return Math.max(0, value)
return 400
}
const scheduleClose = () => {
if (timer) window.clearTimeout(timer)
timer = window.setTimeout(() => {
setStore({ dock: false, closing: false })
timer = undefined
}, closeMs())
}
// Keep stale turn todos from reopening if the model never clears them.
const clear = () => {
const id = params.id
if (!id) return
sync().set("todo", id, [])
}
createEffect(
on(
() => [params.id, todos().length, done(), live()] as const,
([id, count, complete, active], previous) => {
if (raf) cancelAnimationFrame(raf)
raf = undefined
const next = todoState({
count,
done: complete,
live: active,
})
if (!previous || previous[0] !== id) {
if (timer) window.clearTimeout(timer)
timer = undefined
setStore({ sessionID: id, dock: todoDockAtBoundary(next), closing: false, opening: false })
if (next === "clear") clear()
return
}
if (next === "hide") {
if (timer) window.clearTimeout(timer)
timer = undefined
setStore({ dock: false, closing: false, opening: false })
return
}
if (next === "clear") {
if (timer) window.clearTimeout(timer)
timer = undefined
clear()
return
}
if (next === "open") {
if (timer) window.clearTimeout(timer)
timer = undefined
const hidden = !store.dock || store.closing
setStore({ dock: true, closing: false })
if (hidden) {
setStore("opening", true)
raf = requestAnimationFrame(() => {
setStore("opening", false)
raf = undefined
})
return
}
setStore("opening", false)
return
}
setStore({ dock: true, opening: false, closing: true })
if (!timer) scheduleClose()
},
),
)
onCleanup(() => {
if (!timer) return
window.clearTimeout(timer)
})
onCleanup(() => {
if (!raf) return
cancelAnimationFrame(raf)
})
return { return {
blocked, blocked,
questionRequest, questionRequest,
permissionRequest, permissionRequest,
permissionResponding, permissionResponding,
decide, decide,
todos,
dock: () =>
store.sessionID === params.id
? store.dock
: todoDockAtBoundary(todoState({ count: todos().length, done: done(), live: live() })),
closing: () => store.sessionID === params.id && store.closing,
opening: () => store.sessionID === params.id && store.opening,
} }
} }

View file

@ -1,256 +0,0 @@
import type { Todo } from "@opencode-ai/sdk/v2"
import { AnimatedNumber } from "@opencode-ai/ui/animated-number"
import { Checkbox } from "@opencode-ai/ui/checkbox"
import { DockTray } from "@opencode-ai/ui/dock-surface"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { useSpring } from "@opencode-ai/ui/motion-spring"
import { TextReveal } from "@opencode-ai/ui/text-reveal"
import { TextStrikethrough } from "@opencode-ai/ui/text-strikethrough"
import { createResizeObserver } from "@solid-primitives/resize-observer"
import { Index, createEffect, createMemo } from "solid-js"
import { createStore } from "solid-js/store"
import { useLanguage } from "@/context/language"
const doneToken = "\u0000done\u0000"
const totalToken = "\u0000total\u0000"
function dot(status: Todo["status"]) {
if (status !== "in_progress") return undefined
return (
<svg
viewBox="0 0 12 12"
width="12"
height="12"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
class="block"
>
<circle
cx="6"
cy="6"
r="3"
style={{
animation: "var(--animate-pulse-scale)",
"transform-origin": "center",
"transform-box": "fill-box",
}}
/>
</svg>
)
}
export function SessionTodoDock(props: {
todos: Todo[]
collapsed: boolean
onToggle: () => void
collapseLabel: string
expandLabel: string
dockProgress: number
}) {
const language = useLanguage()
const [store, setStore] = createStore({
height: 78,
})
const total = createMemo(() => props.todos.length)
const done = createMemo(() => props.todos.filter((todo) => todo.status === "completed").length)
const label = createMemo(() => language.t("session.todo.progress", { done: done(), total: total() }))
const progress = createMemo(() =>
language
.t("session.todo.progress", { done: doneToken, total: totalToken })
.split(/(\u0000done\u0000|\u0000total\u0000)/),
)
const active = createMemo(
() =>
props.todos.find((todo) => todo.status === "in_progress") ??
props.todos.find((todo) => todo.status === "pending") ??
props.todos.filter((todo) => todo.status === "completed").at(-1) ??
props.todos[0],
)
const preview = createMemo(() => active()?.content ?? "")
const collapse = useSpring(() => (props.collapsed ? 1 : 0), { visualDuration: 0.3, bounce: 0 })
const dock = createMemo(() => Math.max(0, Math.min(1, props.dockProgress)))
const shut = createMemo(() => 1 - dock())
const value = createMemo(() => Math.max(0, Math.min(1, collapse())))
const hide = createMemo(() => Math.max(value(), shut()))
const off = createMemo(() => hide() > 0.98)
const turn = createMemo(() => Math.max(0, Math.min(1, value())))
const full = createMemo(() => Math.max(78, store.height))
let contentRef: HTMLDivElement | undefined
createEffect(() => {
const el = contentRef
if (!el) return
const update = () => {
setStore("height", (height) => Math.max(height, el.scrollHeight))
}
update()
createResizeObserver(el, update)
})
return (
<DockTray
data-component="session-todo-dock"
style={{
"overflow-x": "visible",
"overflow-y": "hidden",
"max-height": `${Math.max(78, full() - value() * (full() - 78))}px`,
}}
>
<div ref={contentRef}>
<div
data-action="session-todo-toggle"
class="pl-3 pr-2 py-2 flex items-center gap-2 overflow-visible"
role="button"
tabIndex={0}
onClick={props.onToggle}
onKeyDown={(event) => {
if (event.key !== "Enter" && event.key !== " ") return
event.preventDefault()
props.onToggle()
}}
>
<span
class="text-14-regular text-text-strong cursor-default inline-flex items-baseline shrink-0 overflow-visible"
aria-label={label()}
style={{
"--tool-motion-odometer-ms": "600ms",
"--tool-motion-mask": "18%",
"--tool-motion-mask-height": "0px",
"--tool-motion-spring-ms": "560ms",
"white-space": "pre",
opacity: `${Math.max(0, Math.min(1, 1 - shut()))}`,
}}
>
<Index each={progress()}>
{(item) =>
item() === doneToken ? (
<AnimatedNumber value={done()} />
) : item() === totalToken ? (
<AnimatedNumber value={total()} />
) : (
<span>{item()}</span>
)
}
</Index>
</span>
<div
data-slot="session-todo-preview"
class="ml-1 min-w-0 overflow-hidden"
style={{
flex: "1 1 auto",
"max-width": "100%",
}}
>
<TextReveal
class="text-14-regular text-text-base cursor-default"
text={props.collapsed ? preview() : undefined}
duration={600}
travel={25}
edge={17}
spring="cubic-bezier(0.34, 1, 0.64, 1)"
springSoft="cubic-bezier(0.34, 1, 0.64, 1)"
growOnly
truncate
/>
</div>
<div class="ml-auto">
<IconButton
data-action="session-todo-toggle-button"
data-collapsed={props.collapsed ? "true" : "false"}
icon="chevron-down"
size="normal"
variant="ghost"
style={{ transform: `rotate(${turn() * 180}deg)` }}
onMouseDown={(event) => {
event.preventDefault()
event.stopPropagation()
}}
onClick={(event) => {
event.stopPropagation()
props.onToggle()
}}
aria-label={props.collapsed ? props.expandLabel : props.collapseLabel}
/>
</div>
</div>
<div
data-slot="session-todo-list"
aria-hidden={props.collapsed || off()}
classList={{
"pointer-events-none": hide() > 0.1,
}}
style={{
visibility: off() ? "hidden" : "visible",
opacity: `${Math.max(0, Math.min(1, 1 - hide()))}`,
}}
>
<TodoList todos={props.todos} />
</div>
</div>
</DockTray>
)
}
function TodoList(props: { todos: Todo[] }) {
const [store, setStore] = createStore({
stuck: false,
})
return (
<div class="relative">
<div
class="px-3 pb-11 flex flex-col gap-1.5 max-h-42 overflow-y-auto no-scrollbar"
style={{ "overflow-anchor": "none" }}
onScroll={(e) => {
setStore("stuck", e.currentTarget.scrollTop > 0)
}}
>
<Index each={props.todos}>
{(todo) => (
<Checkbox
readOnly
checked={todo().status === "completed"}
indeterminate={todo().status === "in_progress"}
data-in-progress={todo().status === "in_progress" ? "" : undefined}
data-state={todo().status}
icon={dot(todo().status)}
style={{
"--checkbox-align": "flex-start",
"--checkbox-offset": "1px",
transition: "opacity 220ms var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1))",
opacity: todo().status === "pending" ? "0.94" : "1",
}}
>
<TextStrikethrough
active={todo().status === "completed" || todo().status === "cancelled"}
text={todo().content}
class="text-14-regular min-w-0 break-words"
style={{
"line-height": "var(--line-height-normal)",
transition:
"color 220ms var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1)), opacity 220ms var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1))",
color:
todo().status === "completed" || todo().status === "cancelled"
? "var(--text-weak)"
: "var(--text-strong)",
opacity: todo().status === "pending" ? "0.92" : "1",
}}
/>
</Checkbox>
)}
</Index>
</div>
<div
class="pointer-events-none absolute top-0 left-0 right-0 h-4 transition-opacity duration-150"
style={{
background: "linear-gradient(to bottom, var(--background-base), transparent)",
opacity: store.stuck ? 1 : 0,
}}
/>
</div>
)
}

View file

@ -1,623 +0,0 @@
// @ts-nocheck
import { createEffect, createMemo, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import type { Todo } from "@opencode-ai/sdk/v2"
import { useServerSync } from "@/context/global-sync"
import { PromptInput } from "@/components/prompt-input"
import { usePrompt } from "@/context/prompt"
import {
SessionComposerRegion,
createSessionComposerController,
createSessionComposerRegionController,
} from "@/pages/session/composer"
export default {
title: "UI/Todo Panel Motion",
id: "components-todo-panel-motion",
tags: ["autodocs"],
parameters: {
docs: {
description: {
component: `### Overview
This playground renders the real session composer region from app code.
### Source path
- \`packages/app/src/pages/session/composer/session-composer-region.tsx\`
### Includes
- \`SessionTodoDock\` (real)
- \`PromptInput\` (real)
No visual reimplementation layer is used for the dock/input stack.`,
},
},
},
}
const pool = [
"Refactor ToolStatusTitle DOM measurement to offscreen global measurer (unconstrained by timeline layout)",
"Remove inline measure nodes/CSS hooks and keep width morph behavior intact",
"Run typechecks/tests and report what changed",
"Verify reduced-motion behavior in timeline",
"Review diff for animation edge cases",
"Document rollout notes in PR description",
"Check keyboard and screen reader semantics",
"Add storybook controls for iteration speed",
]
const btn = (accent?: boolean) =>
({
padding: "6px 14px",
"border-radius": "6px",
border: "1px solid var(--color-divider, #333)",
background: accent ? "var(--color-accent, #58f)" : "var(--color-fill-element, #222)",
color: "var(--color-text, #eee)",
cursor: "pointer",
"font-size": "13px",
}) as const
const controls = {
agents: { available: [], options: ["build"], current: "build", loading: false, visible: true, select: () => {} },
model: {
selection: {
current: () => ({ id: "claude-3-7-sonnet", name: "Claude 3.7 Sonnet", provider: { id: "anthropic" } }),
variant: { list: () => [], current: () => undefined, set: () => {} },
},
paid: true,
loading: false,
},
session: {
id: "story-session",
tabs: { active: () => undefined, all: () => [], open: () => {}, setActive: () => {} },
reviewPanel: { opened: () => false, open: () => {} },
},
newLayoutDesigns: true,
}
const css = `
[data-component="todo-stage"] {
display: grid;
gap: 20px;
padding: 20px;
}
[data-component="todo-preview"] {
height: 560px;
min-height: 0;
}
[data-component="todo-session-root"] {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
background: var(--background-base);
border: 1px solid var(--border-weak-base);
border-radius: 12px;
}
[data-component="todo-session-frame"] {
flex: 1 1 auto;
min-height: 0;
display: flex;
flex-direction: column;
}
[data-component="todo-session-panel"] {
position: relative;
flex: 1 1 auto;
min-height: 0;
height: 100%;
display: flex;
flex-direction: column;
background: var(--background-stronger);
}
[data-slot="todo-preview-content"] {
flex: 1 1 auto;
min-height: 0;
overflow: hidden;
}
[data-slot="todo-preview-scroll"] {
height: 100%;
overflow: auto;
min-height: 0;
padding: 14px 16px;
display: flex;
flex-direction: column;
gap: 10px;
}
[data-slot="todo-preview-spacer"] {
flex: 1 1 auto;
min-height: 0;
}
[data-slot="todo-preview-msg"] {
border-radius: 8px;
border: 1px solid var(--border-weak-base);
background: var(--surface-base);
color: var(--text-weak);
padding: 8px 10px;
font-size: 13px;
line-height: 1.35;
}
[data-slot="todo-preview-msg"][data-strong="true"] {
color: var(--text-strong);
}
`
export const Playground = {
render: () => {
const global = useServerSync()
const prompt = usePrompt()
const [cfg, setCfg] = createStore({
open: true,
collapsed: false,
step: 1,
dockOpenDuration: 0.3,
dockOpenBounce: 0,
dockCloseDuration: 0.3,
dockCloseBounce: 0,
drawerExpandDuration: 0.3,
drawerExpandBounce: 0,
drawerCollapseDuration: 0.3,
drawerCollapseBounce: 0,
subtitleDuration: 600,
subtitleAuto: true,
subtitleTravel: 25,
subtitleEdge: 17,
countDuration: 600,
countMask: 18,
countMaskHeight: 0,
countWidthDuration: 560,
})
const open = () => cfg.open
const step = () => cfg.step
const dockOpenDuration = () => cfg.dockOpenDuration
const dockOpenBounce = () => cfg.dockOpenBounce
const dockCloseDuration = () => cfg.dockCloseDuration
const dockCloseBounce = () => cfg.dockCloseBounce
const drawerExpandDuration = () => cfg.drawerExpandDuration
const drawerExpandBounce = () => cfg.drawerExpandBounce
const drawerCollapseDuration = () => cfg.drawerCollapseDuration
const drawerCollapseBounce = () => cfg.drawerCollapseBounce
const subtitleDuration = () => cfg.subtitleDuration
const subtitleAuto = () => cfg.subtitleAuto
const subtitleTravel = () => cfg.subtitleTravel
const subtitleEdge = () => cfg.subtitleEdge
const countDuration = () => cfg.countDuration
const countMask = () => cfg.countMask
const countMaskHeight = () => cfg.countMaskHeight
const countWidthDuration = () => cfg.countWidthDuration
const state = createSessionComposerController({ closeMs: () => Math.round(dockCloseDuration() * 1000) })
let frame
let scrollRef
const todos = createMemo<Todo[]>(() => {
const done = Math.max(0, Math.min(3, step()))
return pool.slice(0, 3).map((content, i) => ({
id: `todo-${i + 1}`,
content,
status: i < done ? "completed" : i === done && done < 3 ? "in_progress" : "pending",
}))
})
createEffect(() => {
global.todo.set("story-session", todos())
})
const clear = () => {
if (frame) cancelAnimationFrame(frame)
frame = undefined
}
const pin = () => {
if (!scrollRef) return
scrollRef.scrollTop = scrollRef.scrollHeight
}
const collapsed = () => cfg.collapsed
const setCollapsed = (value: boolean) => setCfg("collapsed", value)
const openDock = () => {
clear()
setCfg("open", true)
frame = requestAnimationFrame(() => {
pin()
frame = undefined
})
}
const closeDock = () => {
clear()
setCfg("open", false)
}
const dockOpen = () => open()
const toggleDock = () => {
if (dockOpen()) {
closeDock()
return
}
openDock()
}
const toggleDrawer = () => {
if (!dockOpen()) {
openDock()
frame = requestAnimationFrame(() => {
pin()
setCollapsed(true)
frame = undefined
})
return
}
setCollapsed(!collapsed())
}
const cycle = () => {
setCfg("step", (value) => (value + 1) % 4)
}
onCleanup(clear)
return (
<div data-component="todo-stage">
<style>{css}</style>
<div data-component="todo-preview">
<div data-component="todo-session-root">
<div data-component="todo-session-frame">
<div data-component="todo-session-panel">
<div data-slot="todo-preview-content">
<div data-slot="todo-preview-scroll" class="scroll-view__viewport" ref={scrollRef}>
<div data-slot="todo-preview-spacer" />
<div data-slot="todo-preview-msg" data-strong="true">
Thinking Checking type safety
</div>
<div data-slot="todo-preview-msg">Shell Prints five topic blocks between timed commands</div>
</div>
</div>
<div>
<SessionComposerRegion
controller={createSessionComposerRegionController({
state,
sessionKey: () => "story-session",
sessionID: () => "story-session",
prompt,
ready: () => true,
centered: () => false,
todo: { collapsed, onToggle: () => setCollapsed(!collapsed()) },
followup: () => undefined,
revert: () => undefined,
onResponseSubmit: pin,
openParent: () => {},
setPromptRef: () => {},
setDockRef: () => {},
})}
promptInput={
<PromptInput
controls={controls}
submission={{ abort: () => {}, handleSubmit: (event) => event.preventDefault() }}
ref={() => {}}
newSessionWorktree=""
onNewSessionWorktreeReset={() => {}}
/>
}
/>
</div>
</div>
</div>
</div>
</div>
<div style={{ display: "flex", gap: "8px", "flex-wrap": "wrap" }}>
<button onClick={toggleDock} style={btn(dockOpen())}>
{dockOpen() ? "Animate close" : "Animate open"}
</button>
<button onClick={toggleDrawer} style={btn(dockOpen() && collapsed())}>
{dockOpen() && collapsed() ? "Expand todo dock" : "Collapse todo dock"}
</button>
<button onClick={cycle} style={btn(step() > 0)}>
Cycle progress ({step()}/3 done)
</button>
{[0, 1, 2, 3].map((value) => (
<button onClick={() => setCfg("step", value)} style={btn(step() === value)}>
{value} done
</button>
))}
</div>
<div style={{ display: "grid", gap: "10px", "max-width": "560px" }}>
<div style={{ "font-size": "12px", color: "var(--color-text-secondary, #a3a3a3)" }}>Dock open</div>
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
duration
</span>
<input
type="range"
min="0.1"
max="1"
step="0.01"
value={dockOpenDuration()}
onInput={(event) => setCfg("dockOpenDuration", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
{Math.round(dockOpenDuration() * 1000)}ms
</span>
</label>
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
bounce
</span>
<input
type="range"
min="0"
max="1"
step="0.01"
value={dockOpenBounce()}
onInput={(event) => setCfg("dockOpenBounce", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
{dockOpenBounce().toFixed(2)}
</span>
</label>
<div style={{ "font-size": "12px", color: "var(--color-text-secondary, #a3a3a3)", "margin-top": "4px" }}>
Dock close
</div>
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
duration
</span>
<input
type="range"
min="0.1"
max="1"
step="0.01"
value={dockCloseDuration()}
onInput={(event) => setCfg("dockCloseDuration", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
{Math.round(dockCloseDuration() * 1000)}ms
</span>
</label>
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
bounce
</span>
<input
type="range"
min="0"
max="1"
step="0.01"
value={dockCloseBounce()}
onInput={(event) => setCfg("dockCloseBounce", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
{dockCloseBounce().toFixed(2)}
</span>
</label>
<div style={{ "font-size": "12px", color: "var(--color-text-secondary, #a3a3a3)", "margin-top": "4px" }}>
Drawer expand
</div>
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
duration
</span>
<input
type="range"
min="0.1"
max="1"
step="0.01"
value={drawerExpandDuration()}
onInput={(event) => setCfg("drawerExpandDuration", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
{Math.round(drawerExpandDuration() * 1000)}ms
</span>
</label>
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
bounce
</span>
<input
type="range"
min="0"
max="1"
step="0.01"
value={drawerExpandBounce()}
onInput={(event) => setCfg("drawerExpandBounce", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
{drawerExpandBounce().toFixed(2)}
</span>
</label>
<div style={{ "font-size": "12px", color: "var(--color-text-secondary, #a3a3a3)", "margin-top": "4px" }}>
Drawer collapse
</div>
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
duration
</span>
<input
type="range"
min="0.1"
max="1"
step="0.01"
value={drawerCollapseDuration()}
onInput={(event) => setCfg("drawerCollapseDuration", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
{Math.round(drawerCollapseDuration() * 1000)}ms
</span>
</label>
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
bounce
</span>
<input
type="range"
min="0"
max="1"
step="0.01"
value={drawerCollapseBounce()}
onInput={(event) => setCfg("drawerCollapseBounce", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
{drawerCollapseBounce().toFixed(2)}
</span>
</label>
<div style={{ "font-size": "12px", color: "var(--color-text-secondary, #a3a3a3)", "margin-top": "4px" }}>
Subtitle odometer
</div>
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
duration
</span>
<input
type="range"
min="120"
max="1400"
step="10"
value={subtitleDuration()}
onInput={(event) => setCfg("subtitleDuration", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
{Math.round(subtitleDuration())}ms
</span>
</label>
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
auto fit
</span>
<input
type="checkbox"
checked={subtitleAuto()}
onInput={(event) => setCfg("subtitleAuto", event.currentTarget.checked)}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
{subtitleAuto() ? "on" : "off"}
</span>
</label>
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
travel
</span>
<input
type="range"
min="0"
max="40"
step="1"
value={subtitleTravel()}
onInput={(event) => setCfg("subtitleTravel", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>{subtitleTravel()}px</span>
</label>
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
edge
</span>
<input
type="range"
min="1"
max="40"
step="1"
value={subtitleEdge()}
onInput={(event) => setCfg("subtitleEdge", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>{subtitleEdge()}%</span>
</label>
<div style={{ "font-size": "12px", color: "var(--color-text-secondary, #a3a3a3)", "margin-top": "4px" }}>
Count odometer
</div>
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
duration
</span>
<input
type="range"
min="120"
max="1400"
step="10"
value={countDuration()}
onInput={(event) => setCfg("countDuration", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
{Math.round(countDuration())}ms
</span>
</label>
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
mask
</span>
<input
type="range"
min="4"
max="40"
step="1"
value={countMask()}
onInput={(event) => setCfg("countMask", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>{countMask()}%</span>
</label>
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
mask height
</span>
<input
type="range"
min="0"
max="14"
step="1"
value={countMaskHeight()}
onInput={(event) => setCfg("countMaskHeight", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>{countMaskHeight()}px</span>
</label>
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
width spring
</span>
<input
type="range"
min="0"
max="1200"
step="10"
value={countWidthDuration()}
onInput={(event) => setCfg("countWidthDuration", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }}
/>
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
{Math.round(countWidthDuration())}ms
</span>
</label>
</div>
</div>
)
},
}

View file

@ -10,7 +10,7 @@
// /permission [kind] → triggers a permission request variant // /permission [kind] → triggers a permission request variant
// /question [kind] → triggers a question request variant // /question [kind] → triggers a question request variant
// /fmt <kind> → emits a specific tool/text type (text, reasoning, bash, // /fmt <kind> → emits a specific tool/text type (text, reasoning, bash,
// write, edit, patch, task, todo, question, error, mix) // write, edit, patch, task, question, error, mix)
// //
// Demo mode also handles permission and question replies locally, completing // Demo mode also handles permission and question replies locally, completing
// or failing the synthetic tool parts as appropriate. // or failing the synthetic tool parts as appropriate.
@ -30,7 +30,6 @@ const KINDS = [
"edit", "edit",
"patch", "patch",
"task", "task",
"todo",
"question", "question",
"error", "error",
"mix", "mix",
@ -733,30 +732,6 @@ function emitTask(state: State): void {
}) })
} }
function emitTodo(state: State): void {
const ref = make(state, "todowrite", {
todos: [
{
content: "Trigger permission UI",
status: "completed",
},
{
content: "Trigger question UI",
status: "in_progress",
},
{
content: "Tune tool formatting",
status: "pending",
},
],
})
doneTool(state, ref, {
title: "todowrite",
output: "",
metadata: {},
})
}
function emitQuestionTool(state: State): void { function emitQuestionTool(state: State): void {
const ref = make(state, "question", { const ref = make(state, "question", {
questions: [ questions: [
@ -952,7 +927,6 @@ function emitQuestion(state: State, kind: QuestionKind = "multi"): void {
options: [ options: [
{ label: "Diff", description: "Show an edit diff in the footer" }, { label: "Diff", description: "Show an edit diff in the footer" },
{ label: "Task", description: "Show a structured task summary" }, { label: "Task", description: "Show a structured task summary" },
{ label: "Todo", description: "Show a todo snapshot" },
{ label: "Error", description: "Show an error transcript row" }, { label: "Error", description: "Show an error transcript row" },
], ],
multiple: true, multiple: true,
@ -992,7 +966,6 @@ function emitQuestion(state: State, kind: QuestionKind = "multi"): void {
options: [ options: [
{ label: "Diff", description: "Emit edit diff" }, { label: "Diff", description: "Emit edit diff" },
{ label: "Task", description: "Emit task card" }, { label: "Task", description: "Emit task card" },
{ label: "Todo", description: "Emit todo card" },
], ],
multiple: true, multiple: true,
custom: true, custom: true,
@ -1066,11 +1039,6 @@ async function emitFmt(state: State, kind: string, body: string, signal?: AbortS
return true return true
} }
if (kind === "todo") {
emitTodo(state)
return true
}
if (kind === "question") { if (kind === "question") {
emitQuestionTool(state) emitQuestionTool(state)
return true return true
@ -1091,7 +1059,6 @@ async function emitFmt(state: State, kind: string, body: string, signal?: AbortS
emitEdit(state) emitEdit(state)
emitPatch(state) emitPatch(state)
emitTask(state) emitTask(state)
emitTodo(state)
emitQuestionTool(state) emitQuestionTool(state)
emitError(state, "demo mixed scenario error") emitError(state, "demo mixed scenario error")
return true return true

View file

@ -7,26 +7,6 @@ import { toolFiletype, toolStructuredFinal } from "./tool"
import { RUN_THEME_FALLBACK, transparent, type RunTheme } from "./theme" import { RUN_THEME_FALLBACK, transparent, type RunTheme } from "./theme"
import type { EntryLayout, RunEntryBody, ScrollbackOptions, StreamCommit } from "./types" import type { EntryLayout, RunEntryBody, ScrollbackOptions, StreamCommit } from "./types"
function todoText(item: { status: string; content: string }): string {
if (item.status === "completed") {
return `[✓] ${item.content}`
}
if (item.status === "cancelled") {
return `~[ ] ${item.content}~`
}
if (item.status === "in_progress") {
return `[•] ${item.content}`
}
return `[ ] ${item.content}`
}
function todoColor(theme: RunTheme, status: string) {
return status === "in_progress" ? theme.block.warning : theme.block.muted
}
export function entryGroupKey(commit: StreamCommit): string | undefined { export function entryGroupKey(commit: StreamCommit): string | undefined {
if (!commit.partID) { if (!commit.partID) {
return undefined return undefined
@ -137,10 +117,6 @@ export function RunEntryContent(props: {
const next = structured() const next = structured()
return next?.kind === "task" ? next : undefined return next?.kind === "task" ? next : undefined
}) })
const todo_snapshot = createMemo(() => {
const next = structured()
return next?.kind === "todo" ? next : undefined
})
const question_snapshot = createMemo(() => { const question_snapshot = createMemo(() => {
const next = structured() const next = structured()
return next?.kind === "question" ? next : undefined return next?.kind === "question" ? next : undefined
@ -242,25 +218,6 @@ export function RunEntryContent(props: {
</box> </box>
</box> </box>
</Match> </Match>
<Match when={todo_snapshot()}>
<box width="100%" flexDirection="column" gap={1}>
<text width="100%" wrapMode="word" fg={theme().block.muted}>
# Todos
</text>
<box width="100%" flexDirection="column" gap={0}>
{todo_snapshot()!.items.map((item) => (
<text width="100%" wrapMode="word" fg={todoColor(theme(), item.status)}>
{todoText(item)}
</text>
))}
{todo_snapshot()!.tail ? (
<text width="100%" wrapMode="word" fg={theme().block.muted}>
{todo_snapshot()!.tail}
</text>
) : null}
</box>
</box>
</Match>
<Match when={question_snapshot()}> <Match when={question_snapshot()}>
<box width="100%" flexDirection="column" gap={1}> <box width="100%" flexDirection="column" gap={1}>
<text width="100%" wrapMode="word" fg={theme().block.muted}> <text width="100%" wrapMode="word" fg={theme().block.muted}>

View file

@ -55,7 +55,6 @@ type ToolInput = ToolDict & {
content?: string content?: string
command?: string command?: string
workdir?: string workdir?: string
todos?: Array<{ status?: string; content?: string }>
questions?: Array<{ question?: string }> questions?: Array<{ question?: string }>
diff?: string diff?: string
} }
@ -122,7 +121,6 @@ type ToolName =
| "patch" | "patch"
| "batch" | "batch"
| "task" | "task"
| "todowrite"
| "question" | "question"
| "read" | "read"
| "glob" | "glob"
@ -398,25 +396,6 @@ function runTask(p: ToolProps): ToolInline {
} }
} }
function runTodo(p: ToolProps): ToolInline {
return {
icon: "#",
title: "Todos",
mode: "block",
body: list<{ status?: string; content?: string }>(p.frame.input.todos)
.flatMap((item) => {
const body = typeof item?.content === "string" ? item.content : ""
if (!body) {
return []
}
const mark = item.status === "completed" ? "[✓]" : item.status === "in_progress" ? "[•]" : "[ ]"
return [`${mark} ${body}`]
})
.join("\n"),
}
}
function runSkill(p: ToolProps): ToolInline { function runSkill(p: ToolProps): ToolInline {
return { return {
icon: "→", icon: "→",
@ -604,28 +583,6 @@ function snapTask(p: ToolProps): ToolSnapshot {
} }
} }
function snapTodo(p: ToolProps): ToolSnapshot {
const items = list<{ status?: string; content?: string }>(p.frame.input.todos).flatMap((item) => {
const content = typeof item?.content === "string" ? item.content : ""
if (!content) {
return []
}
return [
{
status: typeof item.status === "string" ? item.status : "",
content,
},
]
})
return {
kind: "todo",
items,
tail: "",
}
}
function snapQuestion(p: ToolProps): ToolSnapshot { function snapQuestion(p: ToolProps): ToolSnapshot {
const answers = list<unknown[]>(p.frame.meta.answers) const answers = list<unknown[]>(p.frame.meta.answers)
const items = list<{ question?: string }>(p.frame.input.questions).map((item, i) => { const items = list<{ question?: string }>(p.frame.input.questions).map((item, i) => {
@ -814,42 +771,6 @@ function scrollTaskFinal(p: ToolProps): string {
return `# ${kind} Task\n${row}` return `# ${kind} Task\n${row}`
} }
function scrollTodoStart(_: ToolProps): string {
return ""
}
function scrollTodoFinal(p: ToolProps): string {
const items = list<{ status?: string }>(p.input.todos)
const time = span(p.frame.state)
if (items.length === 0) {
if (!time) {
return "0 todos"
}
return `0 todos · ${time}`
}
const doneN = items.filter((item) => item.status === "completed").length
const runN = items.filter((item) => item.status === "in_progress").length
const left = items.length - doneN - runN
const tail = [`${items.length} total`]
if (doneN > 0) {
tail.push(`${doneN} done`)
}
if (runN > 0) {
tail.push(`${runN} active`)
}
if (left > 0) {
tail.push(`${left} pending`)
}
if (time) {
tail.push(time)
}
return tail.join(" · ")
}
function scrollQuestionStart(_: ToolProps): string { function scrollQuestionStart(_: ToolProps): string {
return "" return ""
} }
@ -1131,19 +1052,6 @@ const TOOL_RULES = {
}, },
permission: permTask, permission: permTask,
}, },
todowrite: {
view: {
output: false,
final: true,
snap: "structured",
},
run: runTodo,
snap: snapTodo,
scroll: {
start: scrollTodoStart,
final: scrollTodoFinal,
},
},
question: { question: {
view: { view: {
output: false, output: false,

View file

@ -199,15 +199,6 @@ export type ToolTaskSnapshot = {
tail: string tail: string
} }
export type ToolTodoSnapshot = {
kind: "todo"
items: Array<{
status: string
content: string
}>
tail: string
}
export type ToolQuestionSnapshot = { export type ToolQuestionSnapshot = {
kind: "question" kind: "question"
items: Array<{ items: Array<{
@ -217,12 +208,7 @@ export type ToolQuestionSnapshot = {
tail: string tail: string
} }
export type ToolSnapshot = export type ToolSnapshot = ToolCodeSnapshot | ToolDiffSnapshot | ToolTaskSnapshot | ToolQuestionSnapshot
| ToolCodeSnapshot
| ToolDiffSnapshot
| ToolTaskSnapshot
| ToolTodoSnapshot
| ToolQuestionSnapshot
export type EntryLayout = "inline" | "block" export type EntryLayout = "inline" | "block"

View file

@ -5583,17 +5583,6 @@ export type EventSubscribeOutput =
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly id: string; readonly sessionID: string } readonly data: { readonly id: string; readonly sessionID: string }
} }
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "todo.updated"
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
readonly todos: ReadonlyArray<{ readonly content: string; readonly status: string; readonly priority: string }>
}
}
| { | {
readonly id: string readonly id: string
readonly created: number readonly created: number

View file

@ -75,6 +75,62 @@
"summary": "Check server health" "summary": "Check server health"
} }
}, },
"/api/server": {
"get": {
"tags": [
"server"
],
"operationId": "v2.server.get",
"parameters": [],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"urls": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"urls"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Return the URLs that can be used to connect to this server.",
"summary": "Get server information"
}
},
"/api/location": { "/api/location": {
"get": { "get": {
"tags": [ "tags": [
@ -18676,6 +18732,7 @@
"required": [ "required": [
"id", "id",
"sessionID", "sessionID",
"title",
"mode", "mode",
"fields" "fields"
], ],
@ -18714,6 +18771,7 @@
"required": [ "required": [
"id", "id",
"sessionID", "sessionID",
"title",
"mode", "mode",
"url" "url"
], ],
@ -18791,6 +18849,7 @@
} }
}, },
"required": [ "required": [
"title",
"mode" "mode"
], ],
"additionalProperties": false "additionalProperties": false
@ -24584,6 +24643,7 @@
"required": [ "required": [
"id", "id",
"sessionID", "sessionID",
"title",
"mode", "mode",
"fields" "fields"
], ],
@ -24622,6 +24682,7 @@
"required": [ "required": [
"id", "id",
"sessionID", "sessionID",
"title",
"mode", "mode",
"url" "url"
], ],
@ -24844,88 +24905,6 @@
], ],
"additionalProperties": false "additionalProperties": false
}, },
"Todo": {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "Brief description of the task"
},
"status": {
"type": "string",
"description": "Current status of the task: pending, in_progress, completed, cancelled"
},
"priority": {
"type": "string",
"description": "Priority level of the task: high, medium, low"
}
},
"required": [
"content",
"status",
"priority"
],
"additionalProperties": false
},
"todo.updated": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"created": {
"type": "number"
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"todo.updated"
]
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"todos": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Todo"
}
}
},
"required": [
"sessionID",
"todos"
],
"additionalProperties": false
}
},
"required": [
"id",
"created",
"type",
"data"
],
"additionalProperties": false
},
"SessionStatus": { "SessionStatus": {
"anyOf": [ "anyOf": [
{ {
@ -26475,9 +26454,6 @@
{ {
"$ref": "#/components/schemas/form.cancelled" "$ref": "#/components/schemas/form.cancelled"
}, },
{
"$ref": "#/components/schemas/todo.updated"
},
{ {
"$ref": "#/components/schemas/session.status" "$ref": "#/components/schemas/session.status"
}, },
@ -27072,6 +27048,9 @@
{ {
"name": "health" "name": "health"
}, },
{
"name": "server"
},
{ {
"name": "location" "name": "location"
}, },

View file

@ -1,9 +1,9 @@
{ {
"version": "7", "version": "7",
"dialect": "sqlite", "dialect": "sqlite",
"id": "beb91b32-23e2-405f-99b8-1e8763db94f8", "id": "8c1748cc-f978-4df4-879d-fe7fda5c4c34",
"prevIds": [ "prevIds": [
"00000000-0000-0000-0000-000000000000" "beb91b32-23e2-405f-99b8-1e8763db94f8"
], ],
"ddl": [ "ddl": [
{ {
@ -78,10 +78,6 @@
"name": "session", "name": "session",
"entityType": "tables" "entityType": "tables"
}, },
{
"name": "todo",
"entityType": "tables"
},
{ {
"name": "session_share", "name": "session_share",
"entityType": "tables" "entityType": "tables"
@ -1446,76 +1442,6 @@
"entityType": "columns", "entityType": "columns",
"table": "session" "table": "session"
}, },
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "content",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "status",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "priority",
"entityType": "columns",
"table": "todo"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "position",
"entityType": "columns",
"table": "todo"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "todo"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "todo"
},
{ {
"type": "text", "type": "text",
"notNull": false, "notNull": false,
@ -1756,21 +1682,6 @@
"entityType": "fks", "entityType": "fks",
"table": "session" "table": "session"
}, },
{
"columns": [
"session_id"
],
"tableTo": "session",
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_todo_session_id_session_id_fk",
"entityType": "fks",
"table": "todo"
},
{ {
"columns": [ "columns": [
"session_id" "session_id"
@ -1816,16 +1727,6 @@
"entityType": "pks", "entityType": "pks",
"table": "instruction_entry" "table": "instruction_entry"
}, },
{
"columns": [
"session_id",
"position"
],
"nameExplicit": false,
"name": "todo_pk",
"entityType": "pks",
"table": "todo"
},
{ {
"columns": [ "columns": [
"id" "id"
@ -2279,20 +2180,6 @@
"name": "session_parent_idx", "name": "session_parent_idx",
"entityType": "indexes", "entityType": "indexes",
"table": "session" "table": "session"
},
{
"columns": [
{
"value": "session_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "todo_session_idx",
"entityType": "indexes",
"table": "todo"
} }
], ],
"renames": [] "renames": []

View file

@ -50,5 +50,6 @@ export const migrations = (
import("./migration/20260707010146_durable_session_inbox"), import("./migration/20260707010146_durable_session_inbox"),
import("./migration/20260707120000_migrate_prelaunch_v2_state"), import("./migration/20260707120000_migrate_prelaunch_v2_state"),
import("./migration/20260709013000_generic_session_input"), import("./migration/20260709013000_generic_session_input"),
import("./migration/20260709025533_drop-todo"),
]) ])
).map((module) => module.default) satisfies DatabaseMigration.Migration[] ).map((module) => module.default) satisfies DatabaseMigration.Migration[]

View file

@ -0,0 +1,12 @@
import { Effect } from "effect"
import type { DatabaseMigration } from "../migration"
export default {
id: "20260709025533_drop-todo",
up(tx) {
return Effect.gen(function* () {
yield* tx.run(`DROP INDEX IF EXISTS \`todo_session_idx\`;`)
yield* tx.run(`DROP TABLE \`todo\`;`)
})
},
} satisfies DatabaseMigration.Migration

View file

@ -227,19 +227,6 @@ export default {
CONSTRAINT \`fk_session_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE CONSTRAINT \`fk_session_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE
); );
`) `)
yield* tx.run(`
CREATE TABLE \`todo\` (
\`session_id\` text NOT NULL,
\`content\` text NOT NULL,
\`status\` text NOT NULL,
\`priority\` text NOT NULL,
\`position\` integer NOT NULL,
\`time_created\` integer NOT NULL,
\`time_updated\` integer NOT NULL,
CONSTRAINT \`todo_pk\` PRIMARY KEY(\`session_id\`, \`position\`),
CONSTRAINT \`fk_todo_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
);
`)
yield* tx.run(` yield* tx.run(`
CREATE TABLE \`session_share\` ( CREATE TABLE \`session_share\` (
\`session_id\` text PRIMARY KEY, \`session_id\` text PRIMARY KEY,
@ -286,7 +273,6 @@ export default {
yield* tx.run(`CREATE INDEX \`session_project_idx\` ON \`session\` (\`project_id\`);`) yield* tx.run(`CREATE INDEX \`session_project_idx\` ON \`session\` (\`project_id\`);`)
yield* tx.run(`CREATE INDEX \`session_workspace_idx\` ON \`session\` (\`workspace_id\`);`) yield* tx.run(`CREATE INDEX \`session_workspace_idx\` ON \`session\` (\`workspace_id\`);`)
yield* tx.run(`CREATE INDEX \`session_parent_idx\` ON \`session\` (\`parent_id\`);`) yield* tx.run(`CREATE INDEX \`session_parent_idx\` ON \`session\` (\`parent_id\`);`)
yield* tx.run(`CREATE INDEX \`todo_session_idx\` ON \`todo\` (\`session_id\`);`)
}) })
}, },
} satisfies Omit<DatabaseMigration.Migration, "id"> } satisfies Omit<DatabaseMigration.Migration, "id">

View file

@ -32,7 +32,6 @@ import { SessionRunnerLLM } from "./session/runner/llm"
import { SessionRunnerModel } from "./session/runner/model" import { SessionRunnerModel } from "./session/runner/model"
import { SessionCompaction } from "./session/compaction" import { SessionCompaction } from "./session/compaction"
import { SessionTitle } from "./session/title" import { SessionTitle } from "./session/title"
import { SessionTodo } from "./session/todo"
import { SkillV2 } from "./skill" import { SkillV2 } from "./skill"
import { SkillGuidance } from "./skill/guidance" import { SkillGuidance } from "./skill/guidance"
import { Snapshot } from "./snapshot" import { Snapshot } from "./snapshot"
@ -78,7 +77,6 @@ const locationServiceNodes = [
Image.node, Image.node,
SkillGuidance.node, SkillGuidance.node,
ReferenceGuidance.node, ReferenceGuidance.node,
SessionTodo.node,
InstructionEntry.node, InstructionEntry.node,
Form.node, Form.node,
QuestionV2.node, QuestionV2.node,

View file

@ -161,12 +161,7 @@ export const Plugin = define({
item.description = item.description =
"General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel." "General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel."
item.mode = "subagent" item.mode = "subagent"
item.permissions.push( item.permissions.push(...PermissionV2.merge(defaults, [{ action: "subagent", resource: "*", effect: "deny" }]))
...PermissionV2.merge(defaults, [
{ action: "subagent", resource: "*", effect: "deny" },
{ action: "todowrite", resource: "*", effect: "deny" },
]),
)
}) })
draft.update(AgentV2.ID.make("explore"), (item) => { draft.update(AgentV2.ID.make("explore"), (item) => {

View file

@ -28,7 +28,6 @@ import { PermissionV2 } from "../permission"
import { Reference } from "../reference" import { Reference } from "../reference"
import { Ripgrep } from "../ripgrep" import { Ripgrep } from "../ripgrep"
import { SessionInstructions } from "../session/instructions" import { SessionInstructions } from "../session/instructions"
import { SessionTodo } from "../session/todo"
import { Shell } from "../shell" import { Shell } from "../shell"
import { SkillV2 } from "../skill" import { SkillV2 } from "../skill"
import { PatchTool } from "../tool/patch" import { PatchTool } from "../tool/patch"
@ -41,7 +40,6 @@ import { ReadTool } from "../tool/read"
import { ShellTool } from "../tool/shell" import { ShellTool } from "../tool/shell"
import { SkillTool } from "../tool/skill" import { SkillTool } from "../tool/skill"
import { SubagentTool } from "../tool/subagent" import { SubagentTool } from "../tool/subagent"
import { TodoWriteTool } from "../tool/todowrite"
import { Tools } from "../tool/tools" import { Tools } from "../tool/tools"
import { WebFetchTool } from "../tool/webfetch" import { WebFetchTool } from "../tool/webfetch"
import { WebSearchTool } from "../tool/websearch" import { WebSearchTool } from "../tool/websearch"
@ -78,7 +76,6 @@ const services = Effect.fn("PluginInternal.services")(function* () {
const reference = yield* Reference.Service const reference = yield* Reference.Service
const ripgrep = yield* Ripgrep.Service const ripgrep = yield* Ripgrep.Service
const instructions = yield* SessionInstructions.Service const instructions = yield* SessionInstructions.Service
const todo = yield* SessionTodo.Service
const shell = yield* Shell.Service const shell = yield* Shell.Service
const skill = yield* SkillV2.Service const skill = yield* SkillV2.Service
const tools = yield* Tools.Service const tools = yield* Tools.Service
@ -107,7 +104,6 @@ const services = Effect.fn("PluginInternal.services")(function* () {
Context.make(Reference.Service, reference), Context.make(Reference.Service, reference),
Context.make(Ripgrep.Service, ripgrep), Context.make(Ripgrep.Service, ripgrep),
Context.make(SessionInstructions.Service, instructions), Context.make(SessionInstructions.Service, instructions),
Context.make(SessionTodo.Service, todo),
Context.make(Shell.Service, shell), Context.make(Shell.Service, shell),
Context.make(SkillV2.Service, skill), Context.make(SkillV2.Service, skill),
Context.make(Tools.Service, tools), Context.make(Tools.Service, tools),
@ -136,7 +132,6 @@ const pre = [
ShellTool.Plugin, ShellTool.Plugin,
SkillTool.Plugin, SkillTool.Plugin,
SubagentTool.Plugin, SubagentTool.Plugin,
TodoWriteTool.Plugin,
WebFetchTool.Plugin, WebFetchTool.Plugin,
WebSearchTool.Plugin, WebSearchTool.Plugin,
WriteTool.Plugin, WriteTool.Plugin,

View file

@ -30,7 +30,6 @@ import { PluginPromise } from "../plugin/promise"
import { Reference } from "../reference" import { Reference } from "../reference"
import { Ripgrep } from "../ripgrep" import { Ripgrep } from "../ripgrep"
import { SessionInstructions } from "../session/instructions" import { SessionInstructions } from "../session/instructions"
import { SessionTodo } from "../session/todo"
import { Shell } from "../shell" import { Shell } from "../shell"
import { SkillV2 } from "../skill" import { SkillV2 } from "../skill"
import { ReadToolFileSystem } from "../tool/read-filesystem" import { ReadToolFileSystem } from "../tool/read-filesystem"
@ -368,7 +367,6 @@ export const node = makeLocationNode({
Reference.node, Reference.node,
Ripgrep.node, Ripgrep.node,
SessionInstructions.node, SessionInstructions.node,
SessionTodo.node,
Shell.node, Shell.node,
SkillV2.node, SkillV2.node,
ToolRegistry.toolsNode, ToolRegistry.toolsNode,

View file

@ -20,58 +20,8 @@ When the user directly asks about OpenCode (eg. "can OpenCode do...", "does Open
# Professional objectivity # Professional objectivity
Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if OpenCode honestly applies the same rigorous standards to all ideas and disagrees when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs. Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if OpenCode honestly applies the same rigorous standards to all ideas and disagrees when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs.
# Task Management
You have access to the todowrite tool to help you manage and plan tasks. Use it VERY frequently to ensure that you are tracking your tasks and giving the user visibility into your progress.
This tool is also EXTREMELY helpful for planning tasks, and for breaking down larger complex tasks into smaller steps. If you do not use this tool when planning, you may forget to do important tasks - and that is unacceptable.
It is critical that you mark todos as completed as soon as you are done with a task. Do not batch up multiple tasks before marking them as completed.
Examples:
<example>
user: Run the build and fix any type errors
assistant: I'm going to use the todowrite tool to write the following items to the todo list:
- Run the build
- Fix any type errors
I'm now going to run the build using the shell tool.
Looks like I found 10 type errors. I'm going to use the todowrite tool to write 10 items to the todo list.
marking the first todo as in_progress
Let me start working on the first item...
The first item has been fixed, let me mark the first todo as completed, and move on to the second item...
..
..
</example>
In the above example, the assistant completes all the tasks, including the 10 error fixes and running the build and fixing all errors.
<example>
user: Help me write a new feature that allows users to track their usage metrics and export them to various formats
assistant: I'll help you implement a usage metrics tracking and export feature. Let me first use the todowrite tool to plan this task.
Adding the following todos to the todo list:
1. Research existing metrics tracking in the codebase
2. Design the metrics collection system
3. Implement core metrics tracking functionality
4. Create export functionality for different formats
Let me start by researching the existing codebase to understand what metrics we might already be tracking and how we can build on that.
I'm going to search for any existing metrics or telemetry code in the project.
I've found some existing telemetry code. Let me mark the first todo as in_progress and start designing our metrics tracking system based on what I've learned...
[Assistant continues implementing the feature step by step, marking todos as in_progress and completed as they go]
</example>
# Doing tasks # Doing tasks
The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended: The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended:
-
- Use the todowrite tool to plan the task if required
- Tool results and user messages may include <system-reminder> tags. <system-reminder> tags contain useful information and reminders. They are automatically added by the system, and bear no direct relation to the specific tool results or user messages in which they appear. - Tool results and user messages may include <system-reminder> tags. <system-reminder> tags contain useful information and reminders. They are automatically added by the system, and bear no direct relation to the specific tool results or user messages in which they appear.
@ -93,8 +43,6 @@ user: What is the codebase structure?
assistant: [Uses the subagent tool] assistant: [Uses the subagent tool]
</example> </example>
IMPORTANT: Always use the todowrite tool to plan and track tasks throughout the conversation.
# Code References # Code References
When referencing specific functions or pieces of code include the pattern `file_path:line_number` to allow the user to easily navigate to the source code location. When referencing specific functions or pieces of code include the pattern `file_path:line_number` to allow the user to easily navigate to the source code location.

View file

@ -6,7 +6,7 @@ You MUST iterate and keep going until the problem is solved.
You have everything you need to resolve this problem. I want you to fully solve this autonomously before coming back to me. You have everything you need to resolve this problem. I want you to fully solve this autonomously before coming back to me.
Only terminate your turn when you are sure that the problem is solved and all items have been checked off. Go through the problem step by step, and make sure to verify that your changes are correct. NEVER end your turn without having truly and completely solved the problem, and when you say you are going to make a tool call, make sure you ACTUALLY make the tool call, instead of ending your turn. Only terminate your turn when you are sure that the problem is solved. Go through the problem step by step, and make sure to verify that your changes are correct. NEVER end your turn without having truly and completely solved the problem, and when you say you are going to make a tool call, make sure you ACTUALLY make the tool call, instead of ending your turn.
THE PROBLEM CAN NOT BE SOLVED WITHOUT EXTENSIVE INTERNET RESEARCH. THE PROBLEM CAN NOT BE SOLVED WITHOUT EXTENSIVE INTERNET RESEARCH.
@ -19,13 +19,13 @@ understanding of third party packages and dependencies is up to date. You must u
Always tell the user what you are going to do before making a tool call with a single concise sentence. This will help them understand what you are doing and why. Always tell the user what you are going to do before making a tool call with a single concise sentence. This will help them understand what you are doing and why.
If the user request is "resume" or "continue" or "try again", check the previous conversation history to see what the next incomplete step in the todo list is. Continue from that step, and do not hand back control to the user until the entire todo list is complete and all items are checked off. Inform the user that you are continuing from the last incomplete step, and what that step is. If the user request is "resume" or "continue" or "try again", check the previous conversation history to identify the next incomplete step. Continue from that step, and do not hand back control to the user until the request is complete. Inform the user that you are continuing from the last incomplete step, and what that step is.
Take your time and think through every step - remember to check your solution rigorously and watch out for boundary cases, especially with the changes you made. Use the sequential thinking tool if available. Your solution must be perfect. If not, continue working on it. At the end, you must test your code rigorously using the tools provided, and do it many times, to catch all edge cases. If it is not robust, iterate more and make it perfect. Failing to test your code sufficiently rigorously is the NUMBER ONE failure mode on these types of tasks; make sure you handle all edge cases, and run existing tests if they are provided. Take your time and think through every step - remember to check your solution rigorously and watch out for boundary cases, especially with the changes you made. Use the sequential thinking tool if available. Your solution must be perfect. If not, continue working on it. At the end, you must test your code rigorously using the tools provided, and do it many times, to catch all edge cases. If it is not robust, iterate more and make it perfect. Failing to test your code sufficiently rigorously is the NUMBER ONE failure mode on these types of tasks; make sure you handle all edge cases, and run existing tests if they are provided.
You MUST plan extensively before each function call, and reflect extensively on the outcomes of the previous function calls. DO NOT do this entire process by making function calls only, as this can impair your ability to solve the problem and think insightfully. You MUST plan extensively before each function call, and reflect extensively on the outcomes of the previous function calls. DO NOT do this entire process by making function calls only, as this can impair your ability to solve the problem and think insightfully.
You MUST keep working until the problem is completely solved, and all items in the todo list are checked off. Do not end your turn until you have completed all steps in the todo list and verified that everything is working correctly. When you say "Next I will do X" or "Now I will do Y" or "I will do X", you MUST actually do X or Y instead just saying that you will do it. You MUST keep working until the problem is completely solved. Do not end your turn until you have completed the necessary work and verified that everything is working correctly. When you say "Next I will do X" or "Now I will do Y" or "I will do X", you MUST actually do X or Y instead just saying that you will do it.
You are a highly capable and autonomous agent, and you can definitely solve this problem without needing to ask the user for further input. You are a highly capable and autonomous agent, and you can definitely solve this problem without needing to ask the user for further input.
@ -39,7 +39,7 @@ You are a highly capable and autonomous agent, and you can definitely solve this
- What are the dependencies and interactions with other parts of the code? - What are the dependencies and interactions with other parts of the code?
3. Investigate the codebase. Explore relevant files, search for key functions, and gather context. 3. Investigate the codebase. Explore relevant files, search for key functions, and gather context.
4. Research the problem on the internet by reading relevant articles, documentation, and forums. 4. Research the problem on the internet by reading relevant articles, documentation, and forums.
5. Develop a clear, step-by-step plan. Break down the fix into manageable, incremental steps. Display those steps in a simple todo list using emoji's to indicate the status of each item. 5. Develop a clear, step-by-step plan. Break down the fix into manageable, incremental steps.
6. Implement the fix incrementally. Make small, testable code changes. 6. Implement the fix incrementally. Make small, testable code changes.
7. Debug as needed. Use debugging techniques to isolate and resolve issues. 7. Debug as needed. Use debugging techniques to isolate and resolve issues.
8. Test frequently. Run tests after each change to verify correctness. 8. Test frequently. Run tests after each change to verify correctness.
@ -73,10 +73,7 @@ Carefully read the issue and think hard about a plan to solve it before coding.
## 5. Develop a Detailed Plan ## 5. Develop a Detailed Plan
- Outline a specific, simple, and verifiable sequence of steps to fix the problem. - Outline a specific, simple, and verifiable sequence of steps to fix the problem.
- Create a todo list in markdown format to track your progress. - Continue through the planned steps instead of ending your turn and asking the user what they want to do next.
- Each time you complete a step, check it off using `[x]` syntax.
- Each time you check off a step, display the updated todo list to the user.
- Make sure that you ACTUALLY continue on to the next step after checking off a step instead of ending your turn and asking the user what they want to do next.
## 6. Making Code Changes ## 6. Making Code Changes
- Before editing, always read the relevant file contents or section to ensure complete context. - Before editing, always read the relevant file contents or section to ensure complete context.
@ -139,8 +136,6 @@ If you are asked to write a prompt, you should always generate the prompt in ma
If you are not writing the prompt in a file, you should always wrap the prompt in triple backticks so that it is formatted correctly and can be easily copied from the chat. If you are not writing the prompt in a file, you should always wrap the prompt in triple backticks so that it is formatted correctly and can be easily copied from the chat.
Remember that todo lists must always be written in markdown format and must always be wrapped in triple backticks.
# Git # Git
If the user tells you to stage and commit, you may do so. If the user tells you to stage and commit, you may do so.

View file

@ -101,25 +101,6 @@ export const PartTable = sqliteTable(
], ],
) )
export const TodoTable = sqliteTable(
"todo",
{
session_id: text()
.$type<SessionSchema.ID>()
.notNull()
.references(() => SessionTable.id, { onDelete: "cascade" }),
content: text().notNull(),
status: text().notNull(),
priority: text().notNull(),
position: integer().notNull(),
...Timestamps,
},
(table) => [
primaryKey({ columns: [table.session_id, table.position] }),
index("todo_session_idx").on(table.session_id),
],
)
export const SessionMessageTable = sqliteTable( export const SessionMessageTable = sqliteTable(
"session_message", "session_message",
{ {

View file

@ -1,78 +0,0 @@
export * as SessionTodo from "./todo"
import { asc, eq } from "drizzle-orm"
import { Context, Effect, Layer } from "effect"
import { SessionTodo } from "@opencode-ai/schema/session-todo"
import { Database } from "../database/database"
import { makeLocationNode } from "../effect/app-node"
import { EventV2 } from "../event"
import { SessionSchema } from "./schema"
import { TodoTable } from "./sql"
export const Info = SessionTodo.Info
export type Info = typeof Info.Type
export const Event = SessionTodo.Event
export interface Interface {
readonly update: (input: {
readonly sessionID: SessionSchema.ID
readonly todos: ReadonlyArray<Info>
}) => Effect.Effect<void>
readonly get: (sessionID: SessionSchema.ID) => Effect.Effect<ReadonlyArray<Info>>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionTodo") {}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const { db } = yield* Database.Service
const events = yield* EventV2.Service
const update = Effect.fn("SessionTodo.update")(function* (input: {
readonly sessionID: SessionSchema.ID
readonly todos: ReadonlyArray<Info>
}) {
yield* db
.transaction((tx) =>
Effect.gen(function* () {
yield* tx.delete(TodoTable).where(eq(TodoTable.session_id, input.sessionID)).run()
if (input.todos.length === 0) return
yield* tx
.insert(TodoTable)
.values(
input.todos.map((todo, position) => ({
session_id: input.sessionID,
content: todo.content,
status: todo.status,
priority: todo.priority,
position,
})),
)
.run()
}),
)
.pipe(Effect.orDie)
yield* events.publish(Event.Updated, input)
})
const get = Effect.fn("SessionTodo.get")(function* (sessionID: SessionSchema.ID) {
const rows = yield* db
.select()
.from(TodoTable)
.where(eq(TodoTable.session_id, sessionID))
.orderBy(asc(TodoTable.position))
.all()
.pipe(Effect.orDie)
return rows.map((row) => ({
content: row.content,
status: row.status,
priority: row.priority,
}))
})
return Service.of({ update, get })
}),
)
export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node, Database.node] })

View file

@ -1,57 +0,0 @@
export * as TodoWriteTool from "./todowrite"
import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin"
import { ToolFailure } from "@opencode-ai/llm"
import { Effect, Schema } from "effect"
import { PermissionV2 } from "../permission"
import { SessionTodo } from "../session/todo"
import { Tool } from "./tool"
export const name = "todowrite"
export const Input = Schema.Struct({
todos: Schema.Array(SessionTodo.Info).annotate({ description: "The updated todo list" }),
})
export const Output = Schema.Struct({
todos: Schema.Array(SessionTodo.Info),
})
export type Output = typeof Output.Type
export const toModelOutput = (output: Output) => JSON.stringify(output.todos, null, 2)
export const Plugin = {
id: "opencode.tool.todowrite",
effect: Effect.fn("TodoWriteTool.Plugin")(function* (ctx: PluginContext) {
const todos = yield* SessionTodo.Service
const permission = yield* PermissionV2.Service
yield* ctx.tool
.transform((draft) =>
draft.add(
name,
Tool.make({
description:
"Create and maintain a structured task list for the current coding session. Use it to track progress during multi-step work and keep todo statuses current.",
input: Input,
output: Output,
toModelOutput: ({ output }) => [{ type: "text", text: toModelOutput(output) }],
execute: (input, context) =>
Effect.gen(function* () {
yield* permission.assert({
action: name,
resources: ["*"],
save: ["*"],
sessionID: context.sessionID,
agent: context.agent,
source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID },
})
yield* todos.update({ sessionID: context.sessionID, todos: input.todos })
return { todos: input.todos }
}).pipe(Effect.mapError((error) => new ToolFailure({ message: "Unable to update todos", error }))),
}),
),
)
.pipe(Effect.orDie)
}),
}

View file

@ -24,7 +24,6 @@ const InputObject = Schema.StructWithRest(
bash: Schema.optional(Rule), bash: Schema.optional(Rule),
task: Schema.optional(Rule), task: Schema.optional(Rule),
external_directory: Schema.optional(Rule), external_directory: Schema.optional(Rule),
todowrite: Schema.optional(Action),
question: Schema.optional(Action), question: Schema.optional(Action),
webfetch: Schema.optional(Action), webfetch: Schema.optional(Action),
websearch: Schema.optional(Action), websearch: Schema.optional(Action),

File diff suppressed because one or more lines are too long

View file

@ -526,7 +526,6 @@ describe("LocationServiceMap", () => {
"shell", "shell",
"skill", "skill",
"subagent", "subagent",
"todowrite",
"webfetch", "webfetch",
"websearch", "websearch",
"write", "write",
@ -559,7 +558,6 @@ describe("LocationServiceMap", () => {
"shell", "shell",
"skill", "skill",
"subagent", "subagent",
"todowrite",
"webfetch", "webfetch",
"websearch", "websearch",
"write", "write",
@ -577,7 +575,6 @@ describe("LocationServiceMap", () => {
"shell", "shell",
"skill", "skill",
"subagent", "subagent",
"todowrite",
"webfetch", "webfetch",
"websearch", "websearch",
"write", "write",

View file

@ -182,12 +182,12 @@ describe("PermissionV2", () => {
const agents = yield* AgentV2.Service const agents = yield* AgentV2.Service
yield* agents.transform((editor) => yield* agents.transform((editor) =>
editor.update(AgentV2.ID.make("build"), (agent) => { editor.update(AgentV2.ID.make("build"), (agent) => {
agent.permissions = [{ action: "todowrite", resource: "*", effect: "allow" }] agent.permissions = [{ action: "custom", resource: "*", effect: "allow" }]
}), }),
) )
const service = yield* PermissionV2.Service const service = yield* PermissionV2.Service
expect(yield* service.ask(assertion({ action: "todowrite", resources: ["*"] }))).toEqual({ expect(yield* service.ask(assertion({ action: "custom", resources: ["*"] }))).toEqual({
id: PermissionV2.ID.create("per_test"), id: PermissionV2.ID.create("per_test"),
effect: "allow", effect: "allow",
}) })

View file

@ -1,94 +0,0 @@
import { describe, expect } from "bun:test"
import { asc } from "drizzle-orm"
import { Effect } from "effect"
import { Database } from "@opencode-ai/core/database/database"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { EventV2 } from "@opencode-ai/core/event"
import { Project } from "@opencode-ai/core/project"
import { ProjectTable } from "@opencode-ai/core/project/sql"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { SessionV2 } from "@opencode-ai/core/session"
import { SessionTable, TodoTable } from "@opencode-ai/core/session/sql"
import { SessionTodo } from "@opencode-ai/core/session/todo"
import { testEffect } from "./lib/effect"
const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, SessionTodo.node])))
const sessionID = SessionV2.ID.make("ses_todo_test")
const setup = Effect.gen(function* () {
const { db } = yield* Database.Service
yield* db
.insert(ProjectTable)
.values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
.run()
.pipe(Effect.orDie)
yield* db
.insert(SessionTable)
.values({
id: sessionID,
project_id: Project.ID.global,
slug: "todo",
directory: "/project",
title: "todo",
version: "test",
})
.run()
.pipe(Effect.orDie)
})
describe("SessionTodo", () => {
it.effect("replaces persisted todos in order and publishes updates", () =>
Effect.gen(function* () {
yield* setup
const { db } = yield* Database.Service
const events = yield* EventV2.Service
const todos = yield* SessionTodo.Service
const published = new Array<EventV2.Payload>()
const unsubscribe = yield* events.listen((event) =>
Effect.sync(() => {
if (event.type === SessionTodo.Event.Updated.type) published.push(event)
}),
)
yield* Effect.addFinalizer(() => unsubscribe)
yield* todos.update({
sessionID,
todos: [
{ content: "second", status: "pending", priority: "low" },
{ content: "first", status: "in_progress", priority: "high" },
],
})
expect(yield* todos.get(sessionID)).toEqual([
{ content: "second", status: "pending", priority: "low" },
{ content: "first", status: "in_progress", priority: "high" },
])
expect(
(yield* db.select().from(TodoTable).orderBy(asc(TodoTable.position)).all().pipe(Effect.orDie)).map((row) => ({
content: row.content,
position: row.position,
})),
).toEqual([
{ content: "second", position: 0 },
{ content: "first", position: 1 },
])
yield* todos.update({ sessionID, todos: [{ content: "replacement", status: "completed", priority: "medium" }] })
expect(yield* todos.get(sessionID)).toEqual([{ content: "replacement", status: "completed", priority: "medium" }])
yield* todos.update({ sessionID, todos: [] })
expect(yield* todos.get(sessionID)).toEqual([])
expect(published.map((event) => event.data)).toEqual([
{
sessionID,
todos: [
{ content: "second", status: "pending", priority: "low" },
{ content: "first", status: "in_progress", priority: "high" },
],
},
{ sessionID, todos: [{ content: "replacement", status: "completed", priority: "medium" }] },
{ sessionID, todos: [] },
])
}),
)
})

View file

@ -24,7 +24,6 @@ import { LLM } from "@opencode-ai/schema/llm"
import { Permission } from "@opencode-ai/schema/permission" import { Permission } from "@opencode-ai/schema/permission"
import { Pty } from "@opencode-ai/schema/pty" import { Pty } from "@opencode-ai/schema/pty"
import { Reference } from "@opencode-ai/schema/reference" import { Reference } from "@opencode-ai/schema/reference"
import { SessionTodo } from "@opencode-ai/schema/session-todo"
import { Skill } from "@opencode-ai/schema/skill" import { Skill } from "@opencode-ai/schema/skill"
import { AbsolutePath, DateTimeUtcFromMillis, optional, statics } from "@opencode-ai/schema/schema" import { AbsolutePath, DateTimeUtcFromMillis, optional, statics } from "@opencode-ai/schema/schema"
import { ProviderV2 } from "@opencode-ai/core/provider" import { ProviderV2 } from "@opencode-ai/core/provider"
@ -46,7 +45,6 @@ test("Core reuses the canonical shared schemas", async () => {
coreReference, coreReference,
coreSessionInput, coreSessionInput,
coreSessionMessage, coreSessionMessage,
coreSessionTodo,
coreSkill, coreSkill,
coreV2Schema, coreV2Schema,
coreSchema, coreSchema,
@ -67,7 +65,6 @@ test("Core reuses the canonical shared schemas", async () => {
import("@opencode-ai/core/reference"), import("@opencode-ai/core/reference"),
import("@opencode-ai/core/session/input"), import("@opencode-ai/core/session/input"),
import("@opencode-ai/core/session/message"), import("@opencode-ai/core/session/message"),
import("@opencode-ai/core/session/todo"),
import("@opencode-ai/core/skill"), import("@opencode-ai/core/skill"),
import("@opencode-ai/core/v2-schema"), import("@opencode-ai/core/v2-schema"),
import("@opencode-ai/core/schema"), import("@opencode-ai/core/schema"),
@ -160,8 +157,6 @@ test("Core reuses the canonical shared schemas", async () => {
[coreSessionMessage.Assistant, SessionMessage.Assistant], [coreSessionMessage.Assistant, SessionMessage.Assistant],
[coreSessionMessage.Compaction, SessionMessage.Compaction], [coreSessionMessage.Compaction, SessionMessage.Compaction],
[coreSessionMessage.Info, SessionMessage.Info], [coreSessionMessage.Info, SessionMessage.Info],
[coreSessionTodo.Info, SessionTodo.Info],
[coreSessionTodo.Event, SessionTodo.Event],
[coreSkill.DirectorySource, Skill.DirectorySource], [coreSkill.DirectorySource, Skill.DirectorySource],
[coreSkill.UrlSource, Skill.UrlSource], [coreSkill.UrlSource, Skill.UrlSource],
[coreSkill.EmbeddedSource, Skill.EmbeddedSource], [coreSkill.EmbeddedSource, Skill.EmbeddedSource],

View file

@ -1,142 +0,0 @@
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { Database } from "@opencode-ai/core/database/database"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { EventV2 } from "@opencode-ai/core/event"
import { PermissionV2 } from "@opencode-ai/core/permission"
import { Project } from "@opencode-ai/core/project"
import { ProjectTable } from "@opencode-ai/core/project/sql"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { SessionV2 } from "@opencode-ai/core/session"
import { SessionTable } from "@opencode-ai/core/session/sql"
import { SessionTodo } from "@opencode-ai/core/session/todo"
import { TodoWriteTool } from "@opencode-ai/core/tool/todowrite"
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
import { testEffect } from "./lib/effect"
import { makeLocationNode } from "@opencode-ai/core/effect/app-node"
import { toolIdentity, executeTool, registerToolPlugin, settleTool, toolDefinitions } from "./lib/tool"
const todoWriteToolNode = makeLocationNode({
name: "test/todowrite-tool-plugin",
layer: Layer.effectDiscard(registerToolPlugin(TodoWriteTool.Plugin)),
deps: [ToolRegistry.toolsNode, PermissionV2.node, SessionTodo.node],
})
const sessionID = SessionV2.ID.make("ses_todowrite_tool_test")
const assertions: PermissionV2.AssertInput[] = []
let deny = false
const permission = Layer.succeed(
PermissionV2.Service,
PermissionV2.Service.of({
assert: (input) =>
Effect.sync(() => assertions.push(input)).pipe(
Effect.andThen(
deny
? Effect.fail(
new PermissionV2.BlockedError({
rules: [],
permission: input.action,
resources: input.resources,
}),
)
: Effect.void,
),
),
ask: () => Effect.die("unused"),
reply: () => Effect.die("unused"),
get: () => Effect.die("unused"),
forSession: () => Effect.die("unused"),
list: () => Effect.die("unused"),
}),
)
const it = testEffect(
AppNodeBuilder.build(
LayerNode.group([
Database.node,
EventV2.node,
SessionTodo.node,
ToolRegistry.node,
ToolRegistry.toolsNode,
todoWriteToolNode,
]),
[
[PermissionV2.node, permission],
[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
],
),
)
const setup = Effect.gen(function* () {
assertions.length = 0
deny = false
const { db } = yield* Database.Service
yield* db
.insert(ProjectTable)
.values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
.run()
.pipe(Effect.orDie)
yield* db
.insert(SessionTable)
.values({
id: sessionID,
project_id: Project.ID.global,
slug: "todowrite",
directory: "/project",
title: "todowrite",
version: "test",
})
.run()
.pipe(Effect.orDie)
})
const call = (todos: ReadonlyArray<SessionTodo.Info>, id = "call-todowrite") => ({
sessionID,
...toolIdentity,
call: { type: "tool-call" as const, id, name: TodoWriteTool.name, input: { todos } },
})
describe("TodoWriteTool", () => {
it.effect("registers, approves the wildcard resource, persists todos, and returns typed output", () =>
Effect.gen(function* () {
yield* setup
const registry = yield* ToolRegistry.Service
const service = yield* SessionTodo.Service
const todoList: ReadonlyArray<SessionTodo.Info> = [
{ content: "Implement slice", status: "in_progress", priority: "high" },
]
expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual([TodoWriteTool.name])
expect(yield* settleTool(registry, call(todoList))).toEqual({
result: { type: "text", value: JSON.stringify(todoList, null, 2) },
output: {
structured: { todos: todoList },
content: [{ type: "text", text: JSON.stringify(todoList, null, 2) }],
},
})
expect(assertions).toMatchObject([{ sessionID, action: "todowrite", resources: ["*"], save: ["*"] }])
expect(yield* service.get(sessionID)).toEqual(todoList)
}),
)
it.effect("does not update persisted todos when permission is denied", () =>
Effect.gen(function* () {
yield* setup
const registry = yield* ToolRegistry.Service
const service = yield* SessionTodo.Service
yield* service.update({ sessionID, todos: [{ content: "keep", status: "pending", priority: "low" }] })
deny = true
expect(
yield* executeTool(registry, call([{ content: "blocked", status: "completed", priority: "high" }])),
).toEqual({
type: "error",
value: "Unable to update todos",
})
expect(yield* service.get(sessionID)).toEqual([{ content: "keep", status: "pending", priority: "low" }])
expect(assertions).toMatchObject([{ sessionID, action: "todowrite", resources: ["*"], save: ["*"] }])
}),
)
})

View file

@ -75,6 +75,62 @@
"summary": "Check server health" "summary": "Check server health"
} }
}, },
"/api/server": {
"get": {
"tags": [
"server"
],
"operationId": "v2.server.get",
"parameters": [],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"urls": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"urls"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Return the URLs that can be used to connect to this server.",
"summary": "Get server information"
}
},
"/api/location": { "/api/location": {
"get": { "get": {
"tags": [ "tags": [
@ -18676,6 +18732,7 @@
"required": [ "required": [
"id", "id",
"sessionID", "sessionID",
"title",
"mode", "mode",
"fields" "fields"
], ],
@ -18714,6 +18771,7 @@
"required": [ "required": [
"id", "id",
"sessionID", "sessionID",
"title",
"mode", "mode",
"url" "url"
], ],
@ -18791,6 +18849,7 @@
} }
}, },
"required": [ "required": [
"title",
"mode" "mode"
], ],
"additionalProperties": false "additionalProperties": false
@ -24584,6 +24643,7 @@
"required": [ "required": [
"id", "id",
"sessionID", "sessionID",
"title",
"mode", "mode",
"fields" "fields"
], ],
@ -24622,6 +24682,7 @@
"required": [ "required": [
"id", "id",
"sessionID", "sessionID",
"title",
"mode", "mode",
"url" "url"
], ],
@ -24844,88 +24905,6 @@
], ],
"additionalProperties": false "additionalProperties": false
}, },
"Todo": {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "Brief description of the task"
},
"status": {
"type": "string",
"description": "Current status of the task: pending, in_progress, completed, cancelled"
},
"priority": {
"type": "string",
"description": "Priority level of the task: high, medium, low"
}
},
"required": [
"content",
"status",
"priority"
],
"additionalProperties": false
},
"todo.updated": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"created": {
"type": "number"
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"todo.updated"
]
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"todos": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Todo"
}
}
},
"required": [
"sessionID",
"todos"
],
"additionalProperties": false
}
},
"required": [
"id",
"created",
"type",
"data"
],
"additionalProperties": false
},
"SessionStatus": { "SessionStatus": {
"anyOf": [ "anyOf": [
{ {
@ -26475,9 +26454,6 @@
{ {
"$ref": "#/components/schemas/form.cancelled" "$ref": "#/components/schemas/form.cancelled"
}, },
{
"$ref": "#/components/schemas/todo.updated"
},
{ {
"$ref": "#/components/schemas/session.status" "$ref": "#/components/schemas/session.status"
}, },
@ -27072,6 +27048,9 @@
{ {
"name": "health" "name": "health"
}, },
{
"name": "server"
},
{ {
"name": "location" "name": "location"
}, },

View file

@ -50,7 +50,6 @@ These exported tool definitions currently use `Tool.define(...)` in `src/tool`:
- [x] `read.ts` - [x] `read.ts`
- [x] `skill.ts` - [x] `skill.ts`
- [x] `task.ts` - [x] `task.ts`
- [x] `todo.ts`
- [x] `webfetch.ts` - [x] `webfetch.ts`
- [x] `websearch.ts` - [x] `websearch.ts`
- [x] `write.ts` - [x] `write.ts`

View file

@ -381,7 +381,6 @@ Mode pushes are automatically tracked by the plugin runtime. If a plugin is disa
- `vcs?.branch` - `vcs?.branch`
- `session.count()` - `session.count()`
- `session.diff(sessionID)` - `session.diff(sessionID)`
- `session.todo(sessionID)`
- `session.messages(sessionID)` - `session.messages(sessionID)`
- `session.status(sessionID)` - `session.status(sessionID)`
- `session.permission(sessionID)` - `session.permission(sessionID)`
@ -511,12 +510,11 @@ Metadata is persisted by plugin id.
- `internal:sidebar-context` - `internal:sidebar-context`
- `internal:sidebar-mcp` - `internal:sidebar-mcp`
- `internal:sidebar-lsp` - `internal:sidebar-lsp`
- `internal:sidebar-todo`
- `internal:sidebar-files` - `internal:sidebar-files`
- `internal:sidebar-footer` - `internal:sidebar-footer`
- `internal:plugin-manager` - `internal:plugin-manager`
Sidebar content order is currently: context `100`, mcp `200`, lsp `300`, todo `400`, files `500`. Sidebar content order is currently: context `100`, mcp `200`, lsp `300`, files `500`.
The plugin manager is exposed as a command with title `Plugins` and value `plugins.list`. The plugin manager is exposed as a command with title `Plugins` and value `plugins.list`.

View file

@ -182,13 +182,7 @@ const layer = Layer.effect(
general: { general: {
name: "general", name: "general",
description: `General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel.`, description: `General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel.`,
permission: Permission.merge( permission: Permission.merge(defaults, user),
defaults,
Permission.fromConfig({
todowrite: "deny",
}),
user,
),
options: {}, options: {},
mode: "subagent", mode: "subagent",
native: true, native: true,

View file

@ -8,20 +8,18 @@ import type { Agent } from "./agent"
* 1. The parent session's deny rules and external_directory rules. * 1. The parent session's deny rules and external_directory rules.
* Parent agent restrictions only govern that agent; the subagent's own * Parent agent restrictions only govern that agent; the subagent's own
* permissions determine its capabilities. * permissions determine its capabilities.
* 2. Default `todowrite` and `task` denies if the subagent's own ruleset * 2. A default `task` deny if the subagent's own ruleset doesn't already
* doesn't already permit them. * permit it.
*/ */
export function deriveSubagentSessionPermission(input: { export function deriveSubagentSessionPermission(input: {
parentSessionPermission: PermissionV1.Ruleset parentSessionPermission: PermissionV1.Ruleset
subagent: Agent.Info subagent: Agent.Info
}): PermissionV1.Ruleset { }): PermissionV1.Ruleset {
const canTask = input.subagent.permission.some((rule) => rule.permission === "task") const canTask = input.subagent.permission.some((rule) => rule.permission === "task")
const canTodo = input.subagent.permission.some((rule) => rule.permission === "todowrite")
return [ return [
...input.parentSessionPermission.filter( ...input.parentSessionPermission.filter(
(rule) => rule.permission === "external_directory" || rule.action === "deny", (rule) => rule.permission === "external_directory" || rule.action === "deny",
), ),
...(canTodo ? [] : [{ permission: "todowrite" as const, pattern: "*" as const, action: "deny" as const }]),
...(canTask ? [] : [{ permission: "task" as const, pattern: "*" as const, action: "deny" as const }]), ...(canTask ? [] : [{ permission: "task" as const, pattern: "*" as const, action: "deny" as const }]),
] ]
} }

View file

@ -16,19 +16,7 @@ type AgentMode = "all" | "primary" | "subagent"
// Permission keys (not raw tool names). Multiple tools can map to a single // Permission keys (not raw tool names). Multiple tools can map to a single
// permission — e.g. write/edit/apply_patch all gate on `edit` — so we configure // permission — e.g. write/edit/apply_patch all gate on `edit` — so we configure
// agents at the permission level to match how the runtime actually enforces it. // agents at the permission level to match how the runtime actually enforces it.
const AVAILABLE_PERMISSIONS = [ const AVAILABLE_PERMISSIONS = ["bash", "read", "edit", "glob", "grep", "webfetch", "task", "websearch", "lsp", "skill"]
"bash",
"read",
"edit",
"glob",
"grep",
"webfetch",
"task",
"todowrite",
"websearch",
"lsp",
"skill",
]
const AgentCreateCommand = effectCmd({ const AgentCreateCommand = effectCmd({
command: "create", command: "create",

View file

@ -820,7 +820,6 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
async function subscribeSessionEvents() { async function subscribeSessionEvents() {
const TOOL: Record<string, [string, string]> = { const TOOL: Record<string, [string, string]> = {
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
bash: ["Shell", UI.Style.TEXT_DANGER_BOLD], bash: ["Shell", UI.Style.TEXT_DANGER_BOLD],
edit: ["Edit", UI.Style.TEXT_SUCCESS_BOLD], edit: ["Edit", UI.Style.TEXT_SUCCESS_BOLD],
glob: ["Glob", UI.Style.TEXT_INFO_BOLD], glob: ["Glob", UI.Style.TEXT_INFO_BOLD],

View file

@ -20,7 +20,6 @@ import { Skill } from "@/skill"
import { Discovery } from "@/skill/discovery" import { Discovery } from "@/skill/discovery"
import { Question } from "@/question" import { Question } from "@/question"
import { Permission } from "@/permission" import { Permission } from "@/permission"
import { Todo } from "@/session/todo"
import { Session } from "@/session/session" import { Session } from "@/session/session"
import { SessionStatus } from "@/session/status" import { SessionStatus } from "@/session/status"
import { SessionRunState } from "@/session/run-state" import { SessionRunState } from "@/session/run-state"
@ -75,7 +74,6 @@ export const AppLayer = AppNodeBuilderV1.build(
Discovery.node, Discovery.node,
Question.node, Question.node,
Permission.node, Permission.node,
Todo.node,
Session.node, Session.node,
SessionProjector.node, SessionProjector.node,
SessionStatus.node, SessionStatus.node,

View file

@ -8,7 +8,6 @@ import { SessionPrompt } from "@/session/prompt"
import { SessionRevert } from "@/session/revert" import { SessionRevert } from "@/session/revert"
import { SessionStatus } from "@/session/status" import { SessionStatus } from "@/session/status"
import { SessionSummary } from "@/session/summary" import { SessionSummary } from "@/session/summary"
import { Todo } from "@/session/todo"
import { MessageID, PartID, SessionID } from "@/session/schema" import { MessageID, PartID, SessionID } from "@/session/schema"
import { Snapshot } from "@/snapshot" import { Snapshot } from "@/snapshot"
import { Schema, Struct } from "effect" import { Schema, Struct } from "effect"
@ -80,7 +79,6 @@ export const SessionPaths = {
status: `${root}/status`, status: `${root}/status`,
get: `${root}/:sessionID`, get: `${root}/:sessionID`,
children: `${root}/:sessionID/children`, children: `${root}/:sessionID/children`,
todo: `${root}/:sessionID/todo`,
diff: `${root}/:sessionID/diff`, diff: `${root}/:sessionID/diff`,
messages: `${root}/:sessionID/message`, messages: `${root}/:sessionID/message`,
message: `${root}/:sessionID/message/:messageID`, message: `${root}/:sessionID/message/:messageID`,
@ -153,18 +151,6 @@ export const SessionApi = HttpApi.make("session")
description: "Retrieve all child sessions that were forked from the specified parent session.", description: "Retrieve all child sessions that were forked from the specified parent session.",
}), }),
), ),
HttpApiEndpoint.get("todo", SessionPaths.todo, {
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: described(Schema.Array(Todo.Info), "Todo list"),
error: [HttpApiError.BadRequest, ApiNotFoundError],
}).annotateMerge(
OpenApi.annotations({
identifier: "session.todo",
summary: "Get session todos",
description: "Retrieve the todo list associated with a specific session, showing tasks and action items.",
}),
),
HttpApiEndpoint.get("diff", SessionPaths.diff, { HttpApiEndpoint.get("diff", SessionPaths.diff, {
params: { sessionID: SessionID }, params: { sessionID: SessionID },
query: DiffQuery, query: DiffQuery,

View file

@ -13,7 +13,6 @@ import { SessionRevert } from "@/session/revert"
import { SessionRunState } from "@/session/run-state" import { SessionRunState } from "@/session/run-state"
import { SessionStatus } from "@/session/status" import { SessionStatus } from "@/session/status"
import { SessionSummary } from "@/session/summary" import { SessionSummary } from "@/session/summary"
import { Todo } from "@/session/todo"
import { MessageID, PartID, SessionID } from "@/session/schema" import { MessageID, PartID, SessionID } from "@/session/schema"
import { NamedError } from "@opencode-ai/core/util/error" import { NamedError } from "@opencode-ai/core/util/error"
import { Cause, Effect, Option, Schema, Scope } from "effect" import { Cause, Effect, Option, Schema, Scope } from "effect"
@ -56,7 +55,6 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
const agentSvc = yield* Agent.Service const agentSvc = yield* Agent.Service
const permissionSvc = yield* Permission.Service const permissionSvc = yield* Permission.Service
const statusSvc = yield* SessionStatus.Service const statusSvc = yield* SessionStatus.Service
const todoSvc = yield* Todo.Service
const summary = yield* SessionSummary.Service const summary = yield* SessionSummary.Service
const events = yield* EventV2Bridge.Service const events = yield* EventV2Bridge.Service
const scope = yield* Scope.Scope const scope = yield* Scope.Scope
@ -91,11 +89,6 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
return yield* session.children(ctx.params.sessionID) return yield* session.children(ctx.params.sessionID)
}) })
const todo = Effect.fn("SessionHttpApi.todo")(function* (ctx: { params: { sessionID: SessionID } }) {
yield* requireSession(ctx.params.sessionID)
return yield* todoSvc.get(ctx.params.sessionID)
})
const diff = Effect.fn("SessionHttpApi.diff")(function* (ctx: { const diff = Effect.fn("SessionHttpApi.diff")(function* (ctx: {
params: { sessionID: SessionID } params: { sessionID: SessionID }
query: typeof DiffQuery.Type query: typeof DiffQuery.Type
@ -415,7 +408,6 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
.handle("status", status) .handle("status", status)
.handle("get", get) .handle("get", get)
.handle("children", children) .handle("children", children)
.handle("todo", todo)
.handle("diff", diff) .handle("diff", diff)
.handle("messages", messages) .handle("messages", messages)
.handle("message", message) .handle("message", message)

View file

@ -38,7 +38,6 @@ import { SessionRunState } from "@/session/run-state"
import { Session } from "@/session/session" import { Session } from "@/session/session"
import { SessionStatus } from "@/session/status" import { SessionStatus } from "@/session/status"
import { SessionSummary } from "@/session/summary" import { SessionSummary } from "@/session/summary"
import { Todo } from "@/session/todo"
import { SessionShare } from "@/share/session" import { SessionShare } from "@/share/session"
import { ShareNext } from "@/share/share-next" import { ShareNext } from "@/share/share-next"
import { Skill } from "@/skill" import { Skill } from "@/skill"
@ -233,7 +232,6 @@ const app = LayerNode.group([
Question.node, Question.node,
Permission.node, Permission.node,
PermissionSaved.node, PermissionSaved.node,
Todo.node,
Session.node, Session.node,
SessionProjector.node, SessionProjector.node,
SessionStatus.node, SessionStatus.node,

View file

@ -20,58 +20,7 @@ When the user directly asks about OpenCode (eg. "can OpenCode do...", "does Open
# Professional objectivity # Professional objectivity
Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if OpenCode honestly applies the same rigorous standards to all ideas and disagrees when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs. Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if OpenCode honestly applies the same rigorous standards to all ideas and disagrees when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs.
# Task Management
You have access to the TodoWrite tools to help you manage and plan tasks. Use these tools VERY frequently to ensure that you are tracking your tasks and giving the user visibility into your progress.
These tools are also EXTREMELY helpful for planning tasks, and for breaking down larger complex tasks into smaller steps. If you do not use this tool when planning, you may forget to do important tasks - and that is unacceptable.
It is critical that you mark todos as completed as soon as you are done with a task. Do not batch up multiple tasks before marking them as completed.
Examples:
<example>
user: Run the build and fix any type errors
assistant: I'm going to use the TodoWrite tool to write the following items to the todo list:
- Run the build
- Fix any type errors
I'm now going to run the build using Bash.
Looks like I found 10 type errors. I'm going to use the TodoWrite tool to write 10 items to the todo list.
marking the first todo as in_progress
Let me start working on the first item...
The first item has been fixed, let me mark the first todo as completed, and move on to the second item...
..
..
</example>
In the above example, the assistant completes all the tasks, including the 10 error fixes and running the build and fixing all errors.
<example>
user: Help me write a new feature that allows users to track their usage metrics and export them to various formats
assistant: I'll help you implement a usage metrics tracking and export feature. Let me first use the TodoWrite tool to plan this task.
Adding the following todos to the todo list:
1. Research existing metrics tracking in the codebase
2. Design the metrics collection system
3. Implement core metrics tracking functionality
4. Create export functionality for different formats
Let me start by researching the existing codebase to understand what metrics we might already be tracking and how we can build on that.
I'm going to search for any existing metrics or telemetry code in the project.
I've found some existing telemetry code. Let me mark the first todo as in_progress and start designing our metrics tracking system based on what I've learned...
[Assistant continues implementing the feature step by step, marking todos as in_progress and completed as they go]
</example>
# Doing tasks # Doing tasks
The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended:
-
- Use the TodoWrite tool to plan the task if required
- Tool results and user messages may include <system-reminder> tags. <system-reminder> tags contain useful information and reminders. They are automatically added by the system, and bear no direct relation to the specific tool results or user messages in which they appear. - Tool results and user messages may include <system-reminder> tags. <system-reminder> tags contain useful information and reminders. They are automatically added by the system, and bear no direct relation to the specific tool results or user messages in which they appear.
@ -93,8 +42,6 @@ user: What is the codebase structure?
assistant: [Uses the Task tool] assistant: [Uses the Task tool]
</example> </example>
IMPORTANT: Always use the TodoWrite tool to plan and track tasks throughout the conversation.
# Code References # Code References
When referencing specific functions or pieces of code include the pattern `file_path:line_number` to allow the user to easily navigate to the source code location. When referencing specific functions or pieces of code include the pattern `file_path:line_number` to allow the user to easily navigate to the source code location.

View file

@ -6,7 +6,7 @@ You MUST iterate and keep going until the problem is solved.
You have everything you need to resolve this problem. I want you to fully solve this autonomously before coming back to me. You have everything you need to resolve this problem. I want you to fully solve this autonomously before coming back to me.
Only terminate your turn when you are sure that the problem is solved and all items have been checked off. Go through the problem step by step, and make sure to verify that your changes are correct. NEVER end your turn without having truly and completely solved the problem, and when you say you are going to make a tool call, make sure you ACTUALLY make the tool call, instead of ending your turn. Only terminate your turn when you are sure that the problem is solved. Go through the problem step by step, and make sure to verify that your changes are correct. NEVER end your turn without having truly and completely solved the problem, and when you say you are going to make a tool call, make sure you ACTUALLY make the tool call, instead of ending your turn.
THE PROBLEM CAN NOT BE SOLVED WITHOUT EXTENSIVE INTERNET RESEARCH. THE PROBLEM CAN NOT BE SOLVED WITHOUT EXTENSIVE INTERNET RESEARCH.
@ -19,13 +19,13 @@ understanding of third party packages and dependencies is up to date. You must u
Always tell the user what you are going to do before making a tool call with a single concise sentence. This will help them understand what you are doing and why. Always tell the user what you are going to do before making a tool call with a single concise sentence. This will help them understand what you are doing and why.
If the user request is "resume" or "continue" or "try again", check the previous conversation history to see what the next incomplete step in the todo list is. Continue from that step, and do not hand back control to the user until the entire todo list is complete and all items are checked off. Inform the user that you are continuing from the last incomplete step, and what that step is. If the user request is "resume" or "continue" or "try again", check the previous conversation history to identify the next incomplete step. Continue from that step, and do not hand back control to the user until the problem is solved. Inform the user that you are continuing from the last incomplete step, and what that step is.
Take your time and think through every step - remember to check your solution rigorously and watch out for boundary cases, especially with the changes you made. Use the sequential thinking tool if available. Your solution must be perfect. If not, continue working on it. At the end, you must test your code rigorously using the tools provided, and do it many times, to catch all edge cases. If it is not robust, iterate more and make it perfect. Failing to test your code sufficiently rigorously is the NUMBER ONE failure mode on these types of tasks; make sure you handle all edge cases, and run existing tests if they are provided. Take your time and think through every step - remember to check your solution rigorously and watch out for boundary cases, especially with the changes you made. Use the sequential thinking tool if available. Your solution must be perfect. If not, continue working on it. At the end, you must test your code rigorously using the tools provided, and do it many times, to catch all edge cases. If it is not robust, iterate more and make it perfect. Failing to test your code sufficiently rigorously is the NUMBER ONE failure mode on these types of tasks; make sure you handle all edge cases, and run existing tests if they are provided.
You MUST plan extensively before each function call, and reflect extensively on the outcomes of the previous function calls. DO NOT do this entire process by making function calls only, as this can impair your ability to solve the problem and think insightfully. You MUST plan extensively before each function call, and reflect extensively on the outcomes of the previous function calls. DO NOT do this entire process by making function calls only, as this can impair your ability to solve the problem and think insightfully.
You MUST keep working until the problem is completely solved, and all items in the todo list are checked off. Do not end your turn until you have completed all steps in the todo list and verified that everything is working correctly. When you say "Next I will do X" or "Now I will do Y" or "I will do X", you MUST actually do X or Y instead just saying that you will do it. You MUST keep working until the problem is completely solved. Do not end your turn until you have completed all necessary steps and verified that everything is working correctly. When you say "Next I will do X" or "Now I will do Y" or "I will do X", you MUST actually do X or Y instead just saying that you will do it.
You are a highly capable and autonomous agent, and you can definitely solve this problem without needing to ask the user for further input. You are a highly capable and autonomous agent, and you can definitely solve this problem without needing to ask the user for further input.
@ -39,7 +39,7 @@ You are a highly capable and autonomous agent, and you can definitely solve this
- What are the dependencies and interactions with other parts of the code? - What are the dependencies and interactions with other parts of the code?
3. Investigate the codebase. Explore relevant files, search for key functions, and gather context. 3. Investigate the codebase. Explore relevant files, search for key functions, and gather context.
4. Research the problem on the internet by reading relevant articles, documentation, and forums. 4. Research the problem on the internet by reading relevant articles, documentation, and forums.
5. Develop a clear, step-by-step plan. Break down the fix into manageable, incremental steps. Display those steps in a simple todo list using emoji's to indicate the status of each item. 5. Develop a clear, step-by-step plan. Break down the fix into manageable, incremental steps.
6. Implement the fix incrementally. Make small, testable code changes. 6. Implement the fix incrementally. Make small, testable code changes.
7. Debug as needed. Use debugging techniques to isolate and resolve issues. 7. Debug as needed. Use debugging techniques to isolate and resolve issues.
8. Test frequently. Run tests after each change to verify correctness. 8. Test frequently. Run tests after each change to verify correctness.
@ -73,9 +73,6 @@ Carefully read the issue and think hard about a plan to solve it before coding.
## 5. Develop a Detailed Plan ## 5. Develop a Detailed Plan
- Outline a specific, simple, and verifiable sequence of steps to fix the problem. - Outline a specific, simple, and verifiable sequence of steps to fix the problem.
- Create a todo list in markdown format to track your progress.
- Each time you complete a step, check it off using `[x]` syntax.
- Each time you check off a step, display the updated todo list to the user.
- Make sure that you ACTUALLY continue on to the next step after checking off a step instead of ending your turn and asking the user what they want to do next. - Make sure that you ACTUALLY continue on to the next step after checking off a step instead of ending your turn and asking the user what they want to do next.
## 6. Making Code Changes ## 6. Making Code Changes
@ -139,7 +136,6 @@ If you are asked to write a prompt, you should always generate the prompt in ma
If you are not writing the prompt in a file, you should always wrap the prompt in triple backticks so that it is formatted correctly and can be easily copied from the chat. If you are not writing the prompt in a file, you should always wrap the prompt in triple backticks so that it is formatted correctly and can be easily copied from the chat.
Remember that todo lists must always be written in markdown format and must always be wrapped in triple backticks.
# Git # Git
If the user tells you to stage and commit, you may do so. If the user tells you to stage and commit, you may do so.

View file

@ -23,14 +23,14 @@ You must use webfetch tool to recursively gather all information from URL's prov
1. Understand the problem deeply. Carefully read the issue and think critically about what is required. 1. Understand the problem deeply. Carefully read the issue and think critically about what is required.
2. Investigate the codebase. Explore relevant files, search for key functions, and gather context. 2. Investigate the codebase. Explore relevant files, search for key functions, and gather context.
3. Develop a clear, step-by-step plan. Break down the fix into manageable, 3. Develop a clear, step-by-step plan. Break down the fix into manageable,
incremental steps - use the todo tool to track your progress. incremental steps and track your progress.
4. Implement the fix incrementally. Make small, testable code changes. 4. Implement the fix incrementally. Make small, testable code changes.
5. Debug as needed. Use debugging techniques to isolate and resolve issues. 5. Debug as needed. Use debugging techniques to isolate and resolve issues.
6. Test frequently. Run tests after each change to verify correctness. 6. Test frequently. Run tests after each change to verify correctness.
7. Iterate until the root cause is fixed and all tests pass. 7. Iterate until the root cause is fixed and all tests pass.
8. Reflect and validate comprehensively. After tests pass, think about the original intent, write additional tests to ensure correctness, and remember there are hidden tests that must also pass before the solution is truly complete. 8. Reflect and validate comprehensively. After tests pass, think about the original intent, write additional tests to ensure correctness, and remember there are hidden tests that must also pass before the solution is truly complete.
**CRITICAL - Before ending your turn:** **CRITICAL - Before ending your turn:**
- Review and update the todo list, marking completed, skipped (with explanations), or blocked items. - Review the plan, noting completed, skipped (with explanations), or blocked items.
## 1. Deeply Understand the Problem ## 1. Deeply Understand the Problem
- Carefully read the issue and think hard about a plan to solve it before coding. - Carefully read the issue and think hard about a plan to solve it before coding.
@ -50,8 +50,8 @@ incremental steps - use the todo tool to track your progress.
## 3. Develop a Detailed Plan ## 3. Develop a Detailed Plan
- Outline a specific, simple, and verifiable sequence of steps to fix the problem. - Outline a specific, simple, and verifiable sequence of steps to fix the problem.
- Create a todo list to track your progress. - Create a plan to track your progress.
- Each time you check off a step, update the todo list. - Keep the plan current as you complete each step.
- Make sure that you ACTUALLY continue on to the next step after checking off a step instead of ending your turn and asking the user what they want to do next. - Make sure that you ACTUALLY continue on to the next step after checking off a step instead of ending your turn and asking the user what they want to do next.
## 4. Making Code Changes ## 4. Making Code Changes

View file

@ -1,74 +0,0 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionID } from "./schema"
import { Effect, Layer, Context } from "effect"
import { Database } from "@opencode-ai/core/database/database"
import { eq } from "drizzle-orm"
import { asc } from "drizzle-orm"
import { TodoTable } from "@opencode-ai/core/session/sql"
import { EventV2Bridge } from "@/event-v2-bridge"
import { SessionTodo } from "@opencode-ai/schema/session-todo"
export const Info = SessionTodo.Info
export type Info = SessionTodo.Info
export const Event = SessionTodo.Event
export interface Interface {
readonly update: (input: { sessionID: SessionID; todos: ReadonlyArray<Info> }) => Effect.Effect<void>
readonly get: (sessionID: SessionID) => Effect.Effect<Info[]>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionTodo") {}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2Bridge.Service
const { db } = yield* Database.Service
const update = Effect.fn("Todo.update")(function* (input: { sessionID: SessionID; todos: ReadonlyArray<Info> }) {
yield* db
.transaction((tx) =>
Effect.gen(function* () {
yield* tx.delete(TodoTable).where(eq(TodoTable.session_id, input.sessionID)).run()
if (input.todos.length === 0) return
yield* tx
.insert(TodoTable)
.values(
input.todos.map((todo, position) => ({
session_id: input.sessionID,
content: todo.content,
status: todo.status,
priority: todo.priority,
position,
})),
)
.run()
}),
)
.pipe(Effect.orDie)
yield* events.publish(Event.Updated, input)
})
const get = Effect.fn("Todo.get")(function* (sessionID: SessionID) {
const rows = yield* db
.select()
.from(TodoTable)
.where(eq(TodoTable.session_id, sessionID))
.orderBy(asc(TodoTable.position))
.all()
.pipe(Effect.orDie)
return rows.map((row) => ({
content: row.content,
status: row.status,
priority: row.priority,
}))
})
return Service.of({ update, get })
}),
)
export const node = LayerNode.make({ service: Service, layer: layer, deps: [EventV2Bridge.node, Database.node] })
export * as Todo from "./todo"

View file

@ -1,5 +1,5 @@
export { AccountTable, AccountStateTable, ControlAccountTable } from "@opencode-ai/core/account/sql" export { AccountTable, AccountStateTable, ControlAccountTable } from "@opencode-ai/core/account/sql"
export { ProjectTable } from "@opencode-ai/core/project/sql" export { ProjectTable } from "@opencode-ai/core/project/sql"
export { SessionTable, MessageTable, PartTable, TodoTable } from "@opencode-ai/core/session/sql" export { SessionTable, MessageTable, PartTable } from "@opencode-ai/core/session/sql"
export { SessionShareTable } from "@opencode-ai/core/share/sql" export { SessionShareTable } from "@opencode-ai/core/share/sql"
export { WorkspaceTable } from "@opencode-ai/core/control-plane/workspace.sql" export { WorkspaceTable } from "@opencode-ai/core/control-plane/workspace.sql"

View file

@ -11,7 +11,6 @@ import { GrepTool } from "./grep"
import { ReadTool } from "./read" import { ReadTool } from "./read"
import { TaskTool } from "./task" import { TaskTool } from "./task"
import { Database } from "@opencode-ai/core/database/database" import { Database } from "@opencode-ai/core/database/database"
import { TodoWriteTool } from "./todo"
import { WebFetchTool } from "./webfetch" import { WebFetchTool } from "./webfetch"
import { WriteTool } from "./write" import { WriteTool } from "./write"
import { InvalidTool } from "./invalid" import { InvalidTool } from "./invalid"
@ -39,7 +38,6 @@ import { Format } from "../format"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { EffectBridge } from "@/effect/bridge" import { EffectBridge } from "@/effect/bridge"
import { Question } from "../question" import { Question } from "../question"
import { Todo } from "../session/todo"
import { LSP } from "@/lsp/lsp" import { LSP } from "@/lsp/lsp"
import { Instruction } from "../session/instruction" import { Instruction } from "../session/instruction"
import { FSUtil } from "@opencode-ai/core/fs-util" import { FSUtil } from "@opencode-ai/core/fs-util"
@ -97,7 +95,6 @@ const layer = Layer.effect(
const task = yield* TaskTool const task = yield* TaskTool
const read = yield* ReadTool const read = yield* ReadTool
const question = yield* QuestionTool const question = yield* QuestionTool
const todo = yield* TodoWriteTool
const lsptool = yield* LspTool const lsptool = yield* LspTool
const plan = yield* PlanExitTool const plan = yield* PlanExitTool
const webfetch = yield* WebFetchTool const webfetch = yield* WebFetchTool
@ -211,7 +208,6 @@ const layer = Layer.effect(
write: Tool.init(writetool), write: Tool.init(writetool),
task: Tool.init(task), task: Tool.init(task),
fetch: Tool.init(webfetch), fetch: Tool.init(webfetch),
todo: Tool.init(todo),
search: Tool.init(websearch), search: Tool.init(websearch),
skill: Tool.init(skilltool), skill: Tool.init(skilltool),
patch: Tool.init(patchtool), patch: Tool.init(patchtool),
@ -234,7 +230,6 @@ const layer = Layer.effect(
tool.write, tool.write,
tool.task, tool.task,
tool.fetch, tool.fetch,
tool.todo,
tool.search, tool.search,
tool.skill, tool.skill,
tool.patch, tool.patch,
@ -426,7 +421,6 @@ export const node = LayerNode.make({
Config.node, Config.node,
Plugin.node, Plugin.node,
Question.node, Question.node,
Todo.node,
Agent.node, Agent.node,
Skill.node, Skill.node,
Session.node, Session.node,

View file

@ -117,9 +117,6 @@ export const TaskTool = Tool.define(
subagent: next, subagent: next,
}) })
const childToolDenies = [ const childToolDenies = [
...(next.permission.some((rule) => rule.permission === "todowrite")
? []
: [{ permission: "todowrite" as const, pattern: "*" as const, action: "deny" as const }]),
...(next.permission.some((rule) => rule.permission === id) ...(next.permission.some((rule) => rule.permission === id)
? [] ? []
: [{ permission: id, pattern: "*" as const, action: "deny" as const }]), : [{ permission: id, pattern: "*" as const, action: "deny" as const }]),

View file

@ -1,46 +0,0 @@
import { Effect, Schema } from "effect"
import * as Tool from "./tool"
import DESCRIPTION_WRITE from "./todowrite.txt"
import { Todo } from "../session/todo"
export const Parameters = Schema.Struct({
todos: Schema.mutable(Schema.Array(Todo.Info)).annotate({ description: "The updated todo list" }),
})
type Metadata = {
todos: Todo.Info[]
}
export const TodoWriteTool = Tool.define<typeof Parameters, Metadata, Todo.Service>(
"todowrite",
Effect.gen(function* () {
const todo = yield* Todo.Service
return {
description: DESCRIPTION_WRITE,
parameters: Parameters,
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context<Metadata>) =>
Effect.gen(function* () {
yield* ctx.ask({
permission: "todowrite",
patterns: ["*"],
always: ["*"],
metadata: {},
})
yield* todo.update({
sessionID: ctx.sessionID,
todos: params.todos,
})
return {
title: `${params.todos.filter((x) => x.status !== "completed").length} todos`,
output: JSON.stringify(params.todos, null, 2),
metadata: {
todos: params.todos,
},
}
}),
} satisfies Tool.DefWithoutID<typeof Parameters, Metadata>
}),
)

View file

@ -1,44 +0,0 @@
Create and maintain a structured task list for the current coding session. Tracks progress, organizes multi-step work, and surfaces status to the user.
## When to use
Use proactively when:
- The task requires 3+ distinct steps or actions (not just 3 tool calls for a single conceptual step)
- The work is non-trivial and benefits from planning
- The user provides multiple tasks (numbered or comma-separated) or explicitly asks for a todo list
- New instructions arrive - capture them as todos
- You start a task - mark it `in_progress` (only one at a time) before working
- You finish a task - mark it `completed` and add any follow-ups discovered during the work
## When NOT to use
Skip when:
- The work is a single, straightforward task (or <3 trivial steps)
- The request is purely informational or conversational
- Tracking adds no organizational value
## States
- `pending` - not started
- `in_progress` - actively working (exactly ONE at a time)
- `completed` - finished successfully
- `cancelled` - no longer needed
## Rules
- Update status in real time; don't batch completions
- Mark `completed` only after the required work is actually done, including any required verification. Never based on intent.
- Keep exactly one `in_progress` while work remains
- If blocked or partial, keep it `in_progress` and add a follow-up todo describing the blocker
- Preserve user-provided commands verbatim (flags, args, order)
- Items should be specific and actionable; break large work into smaller steps
## Examples
Use it:
- "Add a dark mode toggle and run the tests" -> multi-step feature + explicit verification
- "Rename getCwd -> getCurrentWorkingDirectory across the repo" -> grep reveals 15 occurrences in 8 files
- "Implement registration, catalog, cart, checkout" -> multiple complex features
Skip it:
- "How do I print Hello World in Python?" -> informational
- "Add a comment to calculateTotal" -> single edit
- "Run npm install and tell me what happened" -> one command
When in doubt, use it.

View file

@ -116,7 +116,6 @@ it.instance("explore agent denies edit and write", () =>
expect(explore?.mode).toBe("subagent") expect(explore?.mode).toBe("subagent")
expect(evalPerm(explore, "edit")).toBe("deny") expect(evalPerm(explore, "edit")).toBe("deny")
expect(evalPerm(explore, "write")).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", () => it.instance("compaction agent denies all permissions", () =>
Effect.gen(function* () { Effect.gen(function* () {
const compaction = yield* load((svc) => svc.get("compaction")) const compaction = yield* load((svc) => svc.get("compaction"))

View file

@ -93,7 +93,7 @@ describe("extractResponseText", () => {
}) })
test("returns text even when tool parts follow", () => { 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.") 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)", () => { 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("read", "src/index.ts")]
const parts = [createToolPart("todowrite", "8 todos")]
expect(extractResponseText(parts)).toBeNull() expect(extractResponseText(parts)).toBeNull()
}) })

View file

@ -519,8 +519,8 @@ Options:
--description what the agent should do [string] --description what the agent should do [string]
--mode agent mode [string] [choices: "all", "primary", "subagent"] --mode agent mode [string] [choices: "all", "primary", "subagent"]
--permissions, --tools comma-separated list of permissions to allow (default: all). --permissions, --tools comma-separated list of permissions to allow (default: all).
Available: "bash, read, edit, glob, grep, webfetch, task, todowrite, Available: "bash, read, edit, glob, grep, webfetch, task, websearch,
websearch, lsp, skill" [string] lsp, skill" [string]
-m, --model model to use in the format of provider/model [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 = [ 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", title: "# Questions",
include: ["What should I work on in the codebase next?", "Bug fix"], 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") api.kv.set(options.kv_key, "stored")
const kv_after = api.kv.get(options.kv_key, "missing") const kv_after = api.kv.get(options.kv_key, "missing")
const diff = api.state.session.diff(options.session_id) const diff = api.state.session.diff(options.session_id)
const todo = api.state.session.todo(options.session_id)
const lsp = api.state.lsp() const lsp = api.state.lsp()
const mcp = api.state.mcp() const mcp = api.state.mcp()
const depth_before = api.ui.dialog.depth const depth_before = api.ui.dialog.depth
@ -263,8 +262,6 @@ export default {
kv_ready: api.kv.ready, kv_ready: api.kv.ready,
diff_count: diff.length, diff_count: diff.length,
diff_file: diff[0]?.file, diff_file: diff[0]?.file,
todo_count: todo.length,
todo_first: todo[0]?.content,
lsp_count: lsp.length, lsp_count: lsp.length,
mcp_count: mcp.length, mcp_count: mcp.length,
mcp_first: mcp[0]?.name, mcp_first: mcp[0]?.name,
@ -513,10 +510,6 @@ export default {
if (sessionID !== "ses_test") return [] if (sessionID !== "ses_test") return []
return [{ file: "src/app.ts", additions: 3, deletions: 1 }] return [{ file: "src/app.ts", additions: 3, deletions: 1 }]
}, },
todo(sessionID) {
if (sessionID !== "ses_test") return []
return [{ content: "ship it", status: "pending" }]
},
}, },
lsp() { lsp() {
return [{ id: "ts", root: "/tmp/project", status: "connected" }] 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.kv_ready).toBe(true)
expect(data.local.diff_count).toBe(1) expect(data.local.diff_count).toBe(1)
expect(data.local.diff_file).toBe("src/app.ts") 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.lsp_count).toBe(1)
expect(data.local.mcp_count).toBe(1) expect(data.local.mcp_count).toBe(1)
expect(data.local.mcp_first).toBe("github") expect(data.local.mcp_first).toBe("github")

Some files were not shown because too many files have changed in this diff Show more