Show latest todowrite plan in control panel

This commit is contained in:
Shantur Rathore
2025-12-14 15:05:09 +00:00
parent a6404f25d9
commit 75b3699649
4 changed files with 187 additions and 48 deletions

View File

@@ -10,6 +10,7 @@ import {
type Accessor,
type Component,
} from "solid-js"
import type { ToolState } from "@opencode-ai/sdk"
import { Accordion } from "@kobalte/core"
import { ChevronDown } from "lucide-solid"
import AppBar from "@suid/material/AppBar"
@@ -49,6 +50,7 @@ import AgentSelector from "../agent-selector"
import ModelSelector from "../model-selector"
import CommandPalette from "../command-palette"
import Kbd from "../kbd"
import { TodoListView } from "../tool-call/renderers/todo"
import ContextUsagePanel from "../session/context-usage-panel"
import SessionView from "../session/session-view"
import { formatTokenTotal } from "../../lib/formatters"
@@ -273,6 +275,30 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
}
})
const latestTodoSnapshot = createMemo(() => {
const sessionId = activeSessionIdForInstance()
if (!sessionId || sessionId === "info") return null
const store = messageStore()
if (!store) return null
const snapshot = store.state.latestTodos[sessionId]
return snapshot ?? null
})
const latestTodoState = createMemo<ToolState | null>(() => {
const snapshot = latestTodoSnapshot()
if (!snapshot) return null
const store = messageStore()
if (!store) return null
const message = store.getMessage(snapshot.messageId)
if (!message) return null
const partRecord = message.parts?.[snapshot.partId]
const part = partRecord?.data as { type?: string; tool?: string; state?: ToolState }
if (!part || part.type !== "tool" || part.tool !== "todowrite") return null
const state = part.state
if (!state || state.status !== "completed") return null
return state
})
const connectionStatus = () => sseManager.getStatus(props.instance.id)
const connectionStatusClass = () => {
const status = connectionStatus()
@@ -709,11 +735,23 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
)
const RightDrawerContent = () => {
const renderPlanSectionContent = () => {
const sessionId = activeSessionIdForInstance()
if (!sessionId || sessionId === "info") {
return <p class="text-xs text-secondary">Select a session to view plan.</p>
}
const todoState = latestTodoState()
if (!todoState) {
return <p class="text-xs text-secondary">Nothing planned yet.</p>
}
return <TodoListView state={todoState} emptyLabel="Nothing planned yet." />
}
const sections = [
{
id: "lsp",
label: "LSP Servers",
content: (
render: () => (
<InstanceServiceStatus
instanceId={props.instance.id}
initialInstance={props.instance}
@@ -726,7 +764,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
{
id: "mcp",
label: "MCP Servers",
content: (
render: () => (
<InstanceServiceStatus
instanceId={props.instance.id}
initialInstance={props.instance}
@@ -736,8 +774,19 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
/>
),
},
{
id: "plan",
label: "Plan",
render: renderPlanSectionContent,
},
]
createEffect(() => {
const currentExpanded = new Set(rightPanelExpandedItems())
if (sections.every((section) => currentExpanded.has(section.id))) return
setRightPanelExpandedItems(sections.map((section) => section.id))
})
const handleAccordionChange = (values: string[]) => {
setRightPanelExpandedItems(values)
}
@@ -786,7 +835,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content class="w-full px-3 pb-3 text-sm text-primary">
{section.content}
{section.render()}
</Accordion.Content>
</Accordion.Item>
)}

View File

@@ -5,7 +5,7 @@ import { readToolStatePayload } from "../utils"
export type TodoViewStatus = "pending" | "in_progress" | "completed" | "cancelled"
interface TodoViewItem {
export interface TodoViewItem {
id: string
content: string
status: TodoViewStatus
@@ -58,6 +58,51 @@ function getTodoStatusLabel(status: TodoViewStatus): string {
}
}
interface TodoListViewProps {
state?: ToolState
emptyLabel?: string
}
export function TodoListView(props: TodoListViewProps) {
const todos = extractTodosFromState(props.state)
const counts = summarizeTodos(todos)
if (counts.total === 0) {
return <div class="tool-call-todo-empty">{props.emptyLabel ?? "No plan items yet."}</div>
}
return (
<div class="tool-call-todo-region">
<div class="tool-call-todos" role="list">
<For each={todos}>
{(todo) => {
const label = getTodoStatusLabel(todo.status)
return (
<div
class="tool-call-todo-item"
classList={{
"tool-call-todo-item-completed": todo.status === "completed",
"tool-call-todo-item-cancelled": todo.status === "cancelled",
"tool-call-todo-item-active": todo.status === "in_progress",
}}
role="listitem"
>
<span class="tool-call-todo-checkbox" data-status={todo.status} aria-label={label}></span>
<div class="tool-call-todo-body">
<div class="tool-call-todo-heading">
<span class="tool-call-todo-text">{todo.content}</span>
<span class={`tool-call-todo-status tool-call-todo-status-${todo.status}`}>{label}</span>
</div>
</div>
</div>
)
}}
</For>
</div>
</div>
)
}
export function getTodoTitle(state?: ToolState): string {
if (!state) return "Plan"
@@ -80,42 +125,6 @@ export const todoRenderer: ToolRenderer = {
const state = toolState()
if (!state) return null
const todos = extractTodosFromState(state)
const counts = summarizeTodos(todos)
if (counts.total === 0) {
return <div class="tool-call-todo-empty">No plan items yet.</div>
}
return (
<div class="tool-call-todo-region">
<div class="tool-call-todos" role="list">
<For each={todos}>
{(todo) => {
const label = getTodoStatusLabel(todo.status)
return (
<div
class="tool-call-todo-item"
classList={{
"tool-call-todo-item-completed": todo.status === "completed",
"tool-call-todo-item-cancelled": todo.status === "cancelled",
"tool-call-todo-item-active": todo.status === "in_progress",
}}
role="listitem"
>
<span class="tool-call-todo-checkbox" data-status={todo.status} aria-label={label}></span>
<div class="tool-call-todo-body">
<div class="tool-call-todo-heading">
<span class="tool-call-todo-text">{todo.content}</span>
<span class={`tool-call-todo-status tool-call-todo-status-${todo.status}`}>{label}</span>
</div>
</div>
</div>
)
}}
</For>
</div>
</div>
)
return <TodoListView state={state} />
},
}