refactor: convert coding-agent example from JSX to SFC templates

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yunfei He
2026-05-27 14:55:12 +08:00
parent 72cf46987c
commit 162e022b7f
10 changed files with 196 additions and 242 deletions
+1 -1
View File
@@ -16,7 +16,7 @@
},
"devDependencies": {
"@types/node": "catalog:",
"@vitejs/plugin-vue-jsx": "catalog:",
"@vitejs/plugin-vue": "^6",
"vite": "catalog:"
}
}
-160
View File
@@ -1,160 +0,0 @@
import { shallowRef, defineComponent } from "vue";
import { Box, Text, Static, useInput, useExit } from "@vue-tui/runtime";
import { runAgentLoop, type Message, type ToolCall } from "./agent";
import MessageList from "./components/MessageList";
type AppState = "idle" | "streaming" | "approving";
export default defineComponent(() => {
const state = shallowRef<AppState>("idle");
const inputText = shallowRef("");
const completedMessages = shallowRef<Message[]>([]);
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 = "";
// Persist user message to Static immediately
completedMessages.value = [
...completedMessages.value,
{ role: "user" as const, content: text },
];
try {
const updated = await runAgentLoop(text, messages, {
onToken(token) {
streamingText.value += token;
},
onToolCall(tc: ToolCall, command: string) {
if (streamingText.value) {
completedMessages.value = [
...completedMessages.value,
{ role: "assistant", content: streamingText.value },
];
streamingText.value = "";
}
pendingCommand.value = command;
},
onToolResult(tc: ToolCall, output: string) {
completedMessages.value = [
...completedMessages.value,
{ role: "assistant", tool_calls: [tc] },
{ role: "tool", tool_call_id: tc.id, content: output },
];
pendingCommand.value = "";
},
onComplete() {
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<boolean>((resolve) => {
approvalResolve = resolve;
});
},
});
// Sync full message history
messages.length = 0;
messages.push(...updated);
} catch (err: any) {
const errParts: Message[] = [];
if (streamingText.value) {
errParts.push({ role: "assistant", content: streamingText.value });
}
errParts.push({ role: "assistant", content: `Error: ${err.message}` });
completedMessages.value = [...completedMessages.value, ...errParts];
}
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) {
void 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 () => (
<Box flexDirection="column">
<Static items={completedMessages.value}>
{{
default: ({ item, index }: { item: Message; index: number }) => (
<MessageList key={index} message={item} />
),
}}
</Static>
{streamingText.value && (
<Box>
<Text>{streamingText.value}</Text>
</Box>
)}
{state.value === "approving" && (
<Box borderStyle="round" borderColor="yellow" paddingX={1}>
<Text color="yellow">{pendingCommand.value}</Text>
<Text dimColor>{" [Enter] run / [Esc] skip"}</Text>
</Box>
)}
<Box>
{state.value === "idle" ? (
<Text>
<Text color="cyan">{"> "}</Text>
{inputText.value}
<Text dimColor>{"█"}</Text>
</Text>
) : state.value === "streaming" ? (
<Text dimColor>{"..."}</Text>
) : null}
</Box>
</Box>
);
});
+144
View File
@@ -0,0 +1,144 @@
<script setup lang="ts">
import { shallowRef } from "vue";
import { Box, Text, Static, useInput, useExit } from "@vue-tui/runtime";
import { runAgentLoop, type Message, type ToolCall } from "./agent";
import MessageList from "./components/MessageList.vue";
type AppState = "idle" | "streaming" | "approving";
const state = shallowRef<AppState>("idle");
const inputText = shallowRef("");
const completedMessages = shallowRef<Message[]>([]);
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 = "";
completedMessages.value = [...completedMessages.value, { role: "user" as const, content: text }];
try {
const updated = await runAgentLoop(text, messages, {
onToken(token) {
streamingText.value += token;
},
onToolCall(tc: ToolCall, command: string) {
if (streamingText.value) {
completedMessages.value = [
...completedMessages.value,
{ role: "assistant", content: streamingText.value },
];
streamingText.value = "";
}
pendingCommand.value = command;
},
onToolResult(tc: ToolCall, output: string) {
completedMessages.value = [
...completedMessages.value,
{ role: "assistant", tool_calls: [tc] },
{ role: "tool", tool_call_id: tc.id, content: output },
];
pendingCommand.value = "";
},
onComplete() {
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<boolean>((resolve) => {
approvalResolve = resolve;
});
},
});
messages.length = 0;
messages.push(...updated);
} catch (err: any) {
const errParts: Message[] = [];
if (streamingText.value) {
errParts.push({ role: "assistant", content: streamingText.value });
}
errParts.push({ role: "assistant", content: `Error: ${err.message}` });
completedMessages.value = [...completedMessages.value, ...errParts];
}
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) {
void submit();
} else if (key.backspace || key.delete) {
inputText.value = inputText.value.slice(0, -1);
} else if (input && !key.ctrl && !key.meta) {
inputText.value += input;
}
});
</script>
<template>
<Box flexDirection="column">
<Static :items="completedMessages">
<template #default="{ item, index }">
<MessageList :key="index" :message="item" />
</template>
</Static>
<Box v-if="streamingText">
<Text>{{ streamingText }}</Text>
</Box>
<Box v-if="state === 'approving'" borderStyle="round" borderColor="yellow" :paddingX="1">
<Text color="yellow">{{ pendingCommand }}</Text>
<Text dimColor>{{ " [Enter] run / [Esc] skip" }}</Text>
</Box>
<Box>
<Text v-if="state === 'idle'">
<Text color="cyan">&gt; </Text>{{ inputText }}<Text dimColor></Text>
</Text>
<Text v-else-if="state === 'streaming'" dimColor>...</Text>
</Box>
</Box>
</template>
@@ -1,72 +0,0 @@
import { defineComponent, type PropType } from "vue";
import { Box, Text } from "@vue-tui/runtime";
import type { Message } from "../agent";
export default defineComponent({
props: {
message: { type: Object as PropType<Message>, required: true },
},
setup(props) {
return () => {
const msg = props.message;
if (msg.role === "user") {
return (
<Box>
<Text>
<Text bold color="green">
{"You: "}
</Text>
{msg.content}
</Text>
</Box>
);
}
if (msg.role === "assistant") {
if (msg.tool_calls) {
const parts: any[] = [];
if (msg.content) {
parts.push(
<Text>
<Text bold color="cyan">
{"Agent: "}
</Text>
{msg.content}
</Text>,
);
}
for (const tc of msg.tool_calls) {
const parsed = JSON.parse(tc.function.arguments);
parts.push(
<Box borderStyle="round" borderColor="yellow" paddingX={1}>
<Text color="yellow">{parsed.command}</Text>
</Box>,
);
}
return <Box flexDirection="column">{parts}</Box>;
}
return (
<Box>
<Text>
<Text bold color="cyan">
{"Agent: "}
</Text>
{msg.content}
</Text>
</Box>
);
}
if (msg.role === "tool") {
return (
<Box paddingLeft={2}>
<Text dimColor>{msg.content}</Text>
</Box>
);
}
return null;
};
},
});
@@ -0,0 +1,39 @@
<script setup lang="ts">
import { Box, Text } from "@vue-tui/runtime";
import type { Message } from "../agent";
const { message } = defineProps<{ message: Message }>();
function parseCommand(tc: { function: { arguments: string } }): string {
return JSON.parse(tc.function.arguments).command;
}
</script>
<template>
<Box v-if="message.role === 'user'">
<Text><Text bold color="green">You: </Text>{{ message.content }}</Text>
</Box>
<Box v-else-if="message.role === 'assistant' && message.tool_calls" flexDirection="column">
<Text v-if="message.content">
<Text bold color="cyan">Agent: </Text>{{ message.content }}
</Text>
<Box
v-for="tc in message.tool_calls"
:key="tc.id"
borderStyle="round"
borderColor="yellow"
:paddingX="1"
>
<Text color="yellow">{{ parseCommand(tc) }}</Text>
</Box>
</Box>
<Box v-else-if="message.role === 'assistant'">
<Text><Text bold color="cyan">Agent: </Text>{{ message.content }}</Text>
</Box>
<Box v-else-if="message.role === 'tool'" :paddingLeft="2">
<Text dimColor>{{ message.content }}</Text>
</Box>
</template>
+1 -1
View File
@@ -1,5 +1,5 @@
import { createApp } from "@vue-tui/runtime";
import App from "./App";
import App from "./App.vue";
if (!process.env["DEEPSEEK_API_KEY"]) {
console.error("Error: DEEPSEEK_API_KEY environment variable is required.");
+5
View File
@@ -0,0 +1,5 @@
declare module "*.vue" {
import type { Component } from "vue";
const component: Component;
export default component;
}
+1 -3
View File
@@ -5,9 +5,7 @@
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"jsx": "preserve",
"jsxImportSource": "vue",
"types": ["node"]
},
"include": ["src/**/*.ts", "src/**/*.tsx"]
"include": ["src/**/*.ts", "src/**/*.vue"]
}
+2 -2
View File
@@ -1,11 +1,11 @@
import { defineConfig } from "vite";
import vueJsx from "@vitejs/plugin-vue-jsx";
import vue from "@vitejs/plugin-vue";
import { fileURLToPath } from "node:url";
const here = fileURLToPath(new URL(".", import.meta.url));
export default defineConfig({
plugins: [vueJsx()],
plugins: [vue()],
build: {
target: "node22",
outDir: "dist",
+3 -3
View File
@@ -117,9 +117,9 @@ importers:
'@types/node':
specifier: 'catalog:'
version: 24.12.4
'@vitejs/plugin-vue-jsx':
specifier: 'catalog:'
version: 5.1.5(@voidzero-dev/vite-plus-core@0.1.22(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(vue@3.5.34(typescript@6.0.3))
'@vitejs/plugin-vue':
specifier: ^6
version: 6.0.7(@voidzero-dev/vite-plus-core@0.1.22(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(vue@3.5.34(typescript@6.0.3))
vite:
specifier: npm:@voidzero-dev/vite-plus-core@latest
version: '@voidzero-dev/vite-plus-core@0.1.22(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)'