Files
vue-tui/examples/coding-agent/src/App.tsx
T

173 lines
4.7 KiB
TypeScript

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<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(command) {
// Flush streaming text before tool call
if (streamingText.value) {
completedMessages.value = [
...completedMessages.value,
{ role: "assistant", content: streamingText.value },
];
streamingText.value = "";
}
pendingCommand.value = command;
},
onToolResult(output) {
// Persist the tool call (bordered box) and its output
completedMessages.value = [
...completedMessages.value,
{
role: "assistant",
tool_calls: [
{
id: "",
type: "function",
function: {
name: "bash",
arguments: JSON.stringify({ command: pendingCommand.value }),
},
},
],
},
{ role: "tool", 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) {
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 () => (
<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>
);
});