mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-14 00:10:03 +00:00
todo fmt
This commit is contained in:
parent
9d0cd907f1
commit
30e6543a09
3 changed files with 145 additions and 45 deletions
|
|
@ -11,20 +11,24 @@ import type { EntryLayout, RunEntryBody, ScrollbackOptions, StreamCommit } from
|
|||
|
||||
function todoText(item: { status: string; content: string }): string {
|
||||
if (item.status === "completed") {
|
||||
return `[x] ${item.content}`
|
||||
return `[✓] ${item.content}`
|
||||
}
|
||||
|
||||
if (item.status === "cancelled") {
|
||||
return `[ ] ${item.content} (cancelled)`
|
||||
return `~[ ] ${item.content}~`
|
||||
}
|
||||
|
||||
if (item.status === "in_progress") {
|
||||
return `[ ] ${item.content} (in progress)`
|
||||
return `[•] ${item.content}`
|
||||
}
|
||||
|
||||
return `[ ] ${item.content}`
|
||||
}
|
||||
|
||||
function todoColor(theme: RunTheme, status: string) {
|
||||
return status === "in_progress" ? theme.footer.warning : theme.block.muted
|
||||
}
|
||||
|
||||
export function entryGroupKey(commit: StreamCommit): string | undefined {
|
||||
if (!commit.partID) {
|
||||
return
|
||||
|
|
@ -149,23 +153,28 @@ export function RunEntryContent(props: {
|
|||
}
|
||||
|
||||
if (body().type === "structured") {
|
||||
const snap = snapshot()
|
||||
if (!snap) {
|
||||
return null
|
||||
}
|
||||
|
||||
const width = Math.max(1, Math.trunc(props.width ?? 80))
|
||||
|
||||
if (snapshot()?.kind === "code") {
|
||||
if (snap.kind === "code") {
|
||||
return (
|
||||
<box width="100%" flexDirection="column" gap={1}>
|
||||
<text width="100%" wrapMode="word" fg={theme.block.muted}>
|
||||
{snapshot()?.title}
|
||||
{snap.title}
|
||||
</text>
|
||||
<box width="100%" paddingLeft={1}>
|
||||
<line_number width="100%" fg={theme.block.muted} minWidth={3} paddingRight={1}>
|
||||
<code
|
||||
width="100%"
|
||||
wrapMode="char"
|
||||
filetype={toolFiletype(snapshot()?.file)}
|
||||
filetype={toolFiletype(snap.file)}
|
||||
streaming={false}
|
||||
syntaxStyle={entrySyntax(props.commit, theme)}
|
||||
content={snapshot()?.content}
|
||||
content={snap.content}
|
||||
fg={theme.block.text}
|
||||
/>
|
||||
</line_number>
|
||||
|
|
@ -174,11 +183,11 @@ export function RunEntryContent(props: {
|
|||
)
|
||||
}
|
||||
|
||||
if (snapshot()?.kind === "diff") {
|
||||
if (snap.kind === "diff") {
|
||||
const view = toolDiffView(width, props.opts?.diffStyle)
|
||||
return (
|
||||
<box width="100%" flexDirection="column" gap={1}>
|
||||
{(snapshot()?.items ?? []).map((item) => (
|
||||
{snap.items.map((item) => (
|
||||
<box width="100%" flexDirection="column" gap={1}>
|
||||
<text width="100%" wrapMode="word" fg={theme.block.muted}>
|
||||
{item.title}
|
||||
|
|
@ -216,21 +225,21 @@ export function RunEntryContent(props: {
|
|||
)
|
||||
}
|
||||
|
||||
if (snapshot()?.kind === "task") {
|
||||
if (snap.kind === "task") {
|
||||
return (
|
||||
<box width="100%" flexDirection="column" gap={1}>
|
||||
<text width="100%" wrapMode="word" fg={theme.block.muted}>
|
||||
{snapshot()?.title}
|
||||
{snap.title}
|
||||
</text>
|
||||
<box width="100%" flexDirection="column" gap={0} paddingLeft={1}>
|
||||
{(snapshot()?.rows ?? []).map((row) => (
|
||||
{snap.rows.map((row) => (
|
||||
<text width="100%" wrapMode="word" fg={theme.block.text}>
|
||||
{row}
|
||||
</text>
|
||||
))}
|
||||
{snapshot()?.tail ? (
|
||||
{snap.tail ? (
|
||||
<text width="100%" wrapMode="word" fg={theme.block.muted}>
|
||||
{snapshot()?.tail}
|
||||
{snap.tail}
|
||||
</text>
|
||||
) : null}
|
||||
</box>
|
||||
|
|
@ -238,21 +247,21 @@ export function RunEntryContent(props: {
|
|||
)
|
||||
}
|
||||
|
||||
if (snapshot()?.kind === "todo") {
|
||||
if (snap.kind === "todo") {
|
||||
return (
|
||||
<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} paddingLeft={1}>
|
||||
{(snapshot()?.items ?? []).map((item) => (
|
||||
<text width="100%" wrapMode="word" fg={theme.block.text}>
|
||||
<box width="100%" flexDirection="column" gap={0}>
|
||||
{snap.items.map((item) => (
|
||||
<text width="100%" wrapMode="word" fg={todoColor(theme, item.status)}>
|
||||
{todoText(item)}
|
||||
</text>
|
||||
))}
|
||||
{snapshot()?.tail ? (
|
||||
{snap.tail ? (
|
||||
<text width="100%" wrapMode="word" fg={theme.block.muted}>
|
||||
{snapshot()?.tail}
|
||||
{snap.tail}
|
||||
</text>
|
||||
) : null}
|
||||
</box>
|
||||
|
|
@ -260,13 +269,17 @@ export function RunEntryContent(props: {
|
|||
)
|
||||
}
|
||||
|
||||
if (snap.kind !== "question") {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<box width="100%" flexDirection="column" gap={1}>
|
||||
<text width="100%" wrapMode="word" fg={theme.block.muted}>
|
||||
# Questions
|
||||
</text>
|
||||
<box width="100%" flexDirection="column" gap={1} paddingLeft={1}>
|
||||
{(snapshot()?.items ?? []).map((item) => (
|
||||
{snap.items.map((item) => (
|
||||
<box width="100%" flexDirection="column" gap={0}>
|
||||
<text width="100%" wrapMode="word" fg={theme.block.muted}>
|
||||
{item.question}
|
||||
|
|
@ -276,9 +289,9 @@ export function RunEntryContent(props: {
|
|||
</text>
|
||||
</box>
|
||||
))}
|
||||
{snapshot()?.tail ? (
|
||||
{snap.tail ? (
|
||||
<text width="100%" wrapMode="word" fg={theme.block.muted}>
|
||||
{snapshot()?.tail}
|
||||
{snap.tail}
|
||||
</text>
|
||||
) : null}
|
||||
</box>
|
||||
|
|
|
|||
|
|
@ -391,7 +391,8 @@ function runTodo(p: ToolProps<typeof TodoWriteTool>): ToolInline {
|
|||
return []
|
||||
}
|
||||
|
||||
return [`${item.status === "completed" ? "[x]" : "[ ]"} ${body}`]
|
||||
const mark = item.status === "completed" ? "[✓]" : item.status === "in_progress" ? "[•]" : "[ ]"
|
||||
return [`${mark} ${body}`]
|
||||
})
|
||||
.join("\n"),
|
||||
}
|
||||
|
|
@ -608,24 +609,11 @@ function snapTodo(p: ToolProps<typeof TodoWriteTool>): ToolSnapshot {
|
|||
},
|
||||
]
|
||||
})
|
||||
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`)
|
||||
}
|
||||
|
||||
return {
|
||||
kind: "todo",
|
||||
items,
|
||||
tail: `${done("todos", span(p.frame.state))} · ${tail.join(" · ")}`,
|
||||
tail: "",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -816,13 +804,8 @@ function scrollTaskFinal(p: ToolProps<typeof TaskTool>): string {
|
|||
return rows.join("\n")
|
||||
}
|
||||
|
||||
function scrollTodoStart(p: ToolProps<typeof TodoWriteTool>): string {
|
||||
const todos = p.input.todos ?? []
|
||||
if (todos.length === 0) {
|
||||
return "⚙ Updating todos..."
|
||||
}
|
||||
|
||||
return `⚙ Updating ${todos.length} todo${todos.length === 1 ? "" : "s"}`
|
||||
function scrollTodoStart(_: ToolProps<typeof TodoWriteTool>): string {
|
||||
return ""
|
||||
}
|
||||
|
||||
function scrollTodoFinal(p: ToolProps<typeof TodoWriteTool>): string {
|
||||
|
|
|
|||
|
|
@ -442,6 +442,110 @@ test("inserts a spacer between block assistant entries and following inline tool
|
|||
}
|
||||
})
|
||||
|
||||
test("renders todos without redundant start or footer lines", async () => {
|
||||
const out = await createTestRenderer({
|
||||
width: 80,
|
||||
screenMode: "split-footer",
|
||||
footerHeight: 6,
|
||||
externalOutputMode: "capture-stdout",
|
||||
consoleMode: "disabled",
|
||||
})
|
||||
active.push(out.renderer)
|
||||
|
||||
const scrollback = new RunScrollbackStream(out.renderer, RUN_THEME_FALLBACK, {
|
||||
wrote: false,
|
||||
})
|
||||
|
||||
await scrollback.append({
|
||||
kind: "tool",
|
||||
text: "",
|
||||
phase: "start",
|
||||
source: "tool",
|
||||
partID: "todo-1",
|
||||
messageID: "msg-1",
|
||||
tool: "todowrite",
|
||||
toolState: "running",
|
||||
part: {
|
||||
id: "todo-1",
|
||||
sessionID: "session-1",
|
||||
messageID: "msg-1",
|
||||
type: "tool",
|
||||
callID: "call-1",
|
||||
tool: "todowrite",
|
||||
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,
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
})
|
||||
|
||||
expect(claimCommits(out.renderer)).toHaveLength(0)
|
||||
|
||||
await scrollback.append({
|
||||
kind: "tool",
|
||||
text: "",
|
||||
phase: "final",
|
||||
source: "tool",
|
||||
partID: "todo-1",
|
||||
messageID: "msg-1",
|
||||
tool: "todowrite",
|
||||
toolState: "completed",
|
||||
part: {
|
||||
id: "todo-1",
|
||||
sessionID: "session-1",
|
||||
messageID: "msg-1",
|
||||
type: "tool",
|
||||
callID: "call-1",
|
||||
tool: "todowrite",
|
||||
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,
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
})
|
||||
|
||||
const commits = claimCommits(out.renderer)
|
||||
try {
|
||||
expect(commits).toHaveLength(1)
|
||||
const raw = decoder.decode(commits[0]!.snapshot.getRealCharBytes(true))
|
||||
const rows = Array.from({ length: commits[0]!.snapshot.height }, (_, index) =>
|
||||
raw.slice(index * 80, (index + 1) * 80).trimEnd(),
|
||||
)
|
||||
const rendered = rows.join("\n")
|
||||
expect(rendered).toContain("# Todos")
|
||||
expect(rendered).toContain("[✓] List files under `run/`")
|
||||
expect(rendered).toContain("[•] Count functions in each `run/` file")
|
||||
expect(rendered).toContain("[ ] Mark each tracking item complete")
|
||||
expect(rendered).not.toContain("Updating")
|
||||
expect(rendered).not.toContain("todos completed")
|
||||
expect(rows).toContain("[✓] List files under `run/`")
|
||||
expect(rows).toContain("[•] Count functions in each `run/` file")
|
||||
expect(rows).toContain("[ ] Mark each tracking item complete")
|
||||
} finally {
|
||||
destroyCommits(commits)
|
||||
}
|
||||
})
|
||||
|
||||
test("bodyless starts keep the previous rendered item as separator context", async () => {
|
||||
const out = await createTestRenderer({
|
||||
width: 80,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue