From 6a8a365d249a98fa7ae314674ca1788e32ade554 Mon Sep 17 00:00:00 2001 From: Yunfei He Date: Sun, 24 May 2026 22:32:07 +0800 Subject: [PATCH] feat(coding-agent): implement App with state machine and Static/Dynamic split Co-Authored-By: Claude Opus 4.7 (1M context) --- examples/coding-agent/src/App.tsx | 151 ++++++++++++++++++++++++++++-- 1 file changed, 144 insertions(+), 7 deletions(-) diff --git a/examples/coding-agent/src/App.tsx b/examples/coding-agent/src/App.tsx index 7f1cce9..64e9c74 100644 --- a/examples/coding-agent/src/App.tsx +++ b/examples/coding-agent/src/App.tsx @@ -1,13 +1,150 @@ -import { defineComponent } from "vue"; -import { Box, Text } from "@vue-tui/runtime"; +import { shallowRef, defineComponent } from "vue"; +import { Box, Text, Static, useInput, useExit } from "@vue-tui/runtime"; +import { runAgentLoop, type Message } from "./agent"; +import MessageList from "./components/MessageList"; + +type AppState = "idle" | "streaming" | "approving"; export default defineComponent(() => { + const state = shallowRef("idle"); + const inputText = shallowRef(""); + const completedMessages = shallowRef([]); + const streamingText = shallowRef(""); + const pendingCommand = shallowRef(""); + const messages: Message[] = []; + + let approvalResolve: ((approved: boolean) => void) | null = null; + const exit = useExit(); + + const autoApprove = process.argv.includes("--yolo"); + + async function submit() { + const text = inputText.value.trim(); + if (!text) return; + + inputText.value = ""; + state.value = "streaming"; + streamingText.value = ""; + + try { + const updated = await runAgentLoop(text, messages, { + onToken(token) { + streamingText.value += token; + }, + onToolCall(command) { + // Flush current streaming text as a completed message before showing tool + if (streamingText.value) { + completedMessages.value = [ + ...completedMessages.value, + { role: "assistant", content: streamingText.value }, + ]; + streamingText.value = ""; + } + pendingCommand.value = command; + }, + onToolResult(output) { + // Flush the tool call and result as completed messages + pendingCommand.value = ""; + }, + onComplete() { + // Final streaming text becomes a completed message + if (streamingText.value) { + completedMessages.value = [ + ...completedMessages.value, + { role: "assistant", content: streamingText.value }, + ]; + streamingText.value = ""; + } + }, + autoApprove, + requestApproval(command) { + state.value = "approving"; + pendingCommand.value = command; + return new Promise((resolve) => { + approvalResolve = resolve; + }); + }, + }); + + // Sync full message history + messages.length = 0; + messages.push(...updated); + } catch (err: any) { + completedMessages.value = [ + ...completedMessages.value, + { role: "assistant", content: `Error: ${err.message}` }, + ]; + } + + streamingText.value = ""; + pendingCommand.value = ""; + state.value = "idle"; + } + + useInput((input, key) => { + if (key.ctrl && input === "c") { + exit(); + return; + } + + if (state.value === "approving") { + if (key.return) { + state.value = "streaming"; + approvalResolve?.(true); + approvalResolve = null; + } else if (key.escape) { + state.value = "streaming"; + approvalResolve?.(false); + approvalResolve = null; + } + return; + } + + if (state.value !== "idle") return; + + if (key.return) { + submit(); + } else if (key.backspace || key.delete) { + inputText.value = inputText.value.slice(0, -1); + } else if (input && !key.ctrl && !key.meta) { + inputText.value += input; + } + }); + return () => ( - - - coding-agent - - scaffold works + + + {{ + default: ({ item, index }: { item: Message; index: number }) => ( + + ), + }} + + + {streamingText.value && ( + + {streamingText.value} + + )} + + {state.value === "approving" && ( + + {pendingCommand.value} + {" [Enter] run / [Esc] skip"} + + )} + + + {state.value === "idle" ? ( + + {"> "} + {inputText.value} + {"█"} + + ) : state.value === "streaming" ? ( + {"..."} + ) : null} + ); });