diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 3805c04f..0c6fbdcb 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -40,6 +40,7 @@ - Hook API: `pi.registerShortcut(shortcut, options)` for hooks to register custom keyboard shortcuts (e.g., `shift+p`, `ctrl+shift+x`) - Hook API: `ctx.ui.setWidget(key, lines)` for multi-line status displays above the editor (todo lists, progress tracking) - Hook API: `theme.strikethrough(text)` for strikethrough text styling +- Hook API: `text_delta` event for monitoring streaming assistant text in real-time - `/hotkeys` command now shows hook-registered shortcuts in a separate "Hooks" section - New example hook: `plan-mode.ts` - Claude Code-style read-only exploration mode: - Toggle via `/plan` command, `Shift+P` shortcut, or `--plan` CLI flag @@ -47,6 +48,8 @@ - Bash commands restricted to non-destructive operations (blocks `rm`, `mv`, `git commit`, `npm install`, etc.) - Interactive prompt after each response: execute plan, stay in plan mode, or refine - Todo list widget showing progress with checkboxes and strikethrough for completed items + - Each todo has a unique ID; agent marks items done by outputting `[DONE:id]` + - Real-time progress updates via streaming text monitoring - `/todos` command to view current plan progress - Shows `⏸ plan` indicator in footer when in plan mode, `📋 2/5` when executing - State persists across sessions (including todo progress) diff --git a/packages/coding-agent/docs/hooks.md b/packages/coding-agent/docs/hooks.md index c062af0e..1b6b57c9 100644 --- a/packages/coding-agent/docs/hooks.md +++ b/packages/coding-agent/docs/hooks.md @@ -306,6 +306,21 @@ pi.on("turn_end", async (event, ctx) => { }); ``` +#### text_delta + +Fired for each chunk of streaming text from the assistant. Useful for real-time monitoring of agent output. + +```typescript +pi.on("text_delta", async (event, ctx) => { + // event.text - the new text chunk + + // Example: watch for specific patterns in streaming output + if (event.text.includes("[DONE:")) { + // Handle completion marker + } +}); +``` + #### context Fired before each LLM call. Modify messages non-destructively (session unchanged). diff --git a/packages/coding-agent/examples/hooks/plan-mode.ts b/packages/coding-agent/examples/hooks/plan-mode.ts index 1d6abad4..ee9c2a46 100644 --- a/packages/coding-agent/examples/hooks/plan-mode.ts +++ b/packages/coding-agent/examples/hooks/plan-mode.ts @@ -291,12 +291,25 @@ export default function planModeHook(pi: HookAPI) { } }); - // Check for [DONE:id] tags after each tool result (agent may output them in tool-related text) - pi.on("tool_result", async (_event, ctx) => { + // Watch for [DONE:id] tags in streaming text + pi.on("text_delta", async (event, ctx) => { if (!executionMode || todoItems.length === 0) return; - // The actual checking happens in agent_end when we have the full message - // But we update status here to keep UI responsive - updateStatus(ctx); + + const doneIds = findDoneTags(event.text); + if (doneIds.length === 0) return; + + let changed = false; + for (const id of doneIds) { + const item = todoItems.find((t) => t.id === id); + if (item && !item.completed) { + item.completed = true; + changed = true; + } + } + + if (changed) { + updateStatus(ctx); + } }); // Inject plan mode context diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 689d6ca7..8bf3d96b 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -224,6 +224,15 @@ export class AgentSession { /** Internal handler for agent events - shared by subscribe and reconnect */ private _handleAgentEvent = async (event: AgentEvent): Promise => { + // Emit text_delta events to hooks for streaming text monitoring + if ( + event.type === "message_update" && + event.assistantMessageEvent.type === "text_delta" && + this._hookRunner + ) { + await this._hookRunner.emit({ type: "text_delta", text: event.assistantMessageEvent.delta }); + } + // When a user message starts, check if it's from either queue and remove it BEFORE emitting // This ensures the UI sees the updated queue state if (event.type === "message_start" && event.message.role === "user") { diff --git a/packages/coding-agent/src/core/hooks/types.ts b/packages/coding-agent/src/core/hooks/types.ts index 4f336aee..f2a895fa 100644 --- a/packages/coding-agent/src/core/hooks/types.ts +++ b/packages/coding-agent/src/core/hooks/types.ts @@ -392,6 +392,16 @@ export interface AgentEndEvent { messages: AgentMessage[]; } +/** + * Event data for text_delta event. + * Fired when new text is streamed from the assistant. + */ +export interface TextDeltaEvent { + type: "text_delta"; + /** The new text chunk */ + text: string; +} + /** * Event data for turn_start event. */ @@ -535,6 +545,7 @@ export type HookEvent = | BeforeAgentStartEvent | AgentStartEvent | AgentEndEvent + | TextDeltaEvent | TurnStartEvent | TurnEndEvent | ToolCallEvent @@ -701,6 +712,7 @@ export interface HookAPI { on(event: "turn_end", handler: HookHandler): void; on(event: "tool_call", handler: HookHandler): void; on(event: "tool_result", handler: HookHandler): void; + on(event: "text_delta", handler: HookHandler): void; /** * Send a custom message to the session. Creates a CustomMessageEntry that