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:
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@vitejs/plugin-vue-jsx": "catalog:",
|
||||
"@vitejs/plugin-vue": "^6",
|
||||
"vite": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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">> </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,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
@@ -0,0 +1,5 @@
|
||||
declare module "*.vue" {
|
||||
import type { Component } from "vue";
|
||||
const component: Component;
|
||||
export default component;
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+3
-3
@@ -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)'
|
||||
|
||||
Reference in New Issue
Block a user