vole-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/app.js ADDED
@@ -0,0 +1,702 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ CliChatSession,
4
+ loadConfig,
5
+ renderToolResult
6
+ } from "./chunk-RPVF2IWG.js";
7
+
8
+ // src/app.tsx
9
+ import { useState, useEffect, useCallback, useMemo, useRef as useRef2 } from "react";
10
+ import { render, Box, Text as Text2, useInput, useApp, useAnimation, useStdout, Static } from "ink";
11
+ import TextInput from "ink-text-input";
12
+
13
+ // src/Markdown.tsx
14
+ import { marked as marked2 } from "marked";
15
+ import { useRef } from "react";
16
+ import { Text } from "ink";
17
+
18
+ // src/markdownFormat.ts
19
+ import chalk from "chalk";
20
+ import { marked } from "marked";
21
+ var configured = false;
22
+ function configureMarked() {
23
+ if (configured) return;
24
+ configured = true;
25
+ marked.use({
26
+ tokenizer: {
27
+ del() {
28
+ return void 0;
29
+ }
30
+ }
31
+ });
32
+ }
33
+ function formatToken(token, listDepth = 0, ordered = false) {
34
+ switch (token.type) {
35
+ case "heading": {
36
+ const inner = (token.tokens ?? []).map((t) => formatToken(t)).join("");
37
+ if (token.depth === 1) return chalk.bold.underline(inner) + "\n\n";
38
+ if (token.depth === 2) return chalk.bold(inner) + "\n\n";
39
+ return chalk.bold(chalk.dim(inner)) + "\n\n";
40
+ }
41
+ case "paragraph": {
42
+ const inner = (token.tokens ?? []).map((t) => formatToken(t)).join("");
43
+ return inner + "\n\n";
44
+ }
45
+ case "strong": {
46
+ const inner = (token.tokens ?? []).map((t) => formatToken(t)).join("");
47
+ return chalk.bold(inner);
48
+ }
49
+ case "em": {
50
+ const inner = (token.tokens ?? []).map((t) => formatToken(t)).join("");
51
+ return chalk.italic(inner);
52
+ }
53
+ case "del": {
54
+ const inner = (token.tokens ?? []).map((t) => formatToken(t)).join("");
55
+ return chalk.strikethrough(inner);
56
+ }
57
+ case "codespan": {
58
+ return chalk.yellow(token.text);
59
+ }
60
+ case "code": {
61
+ const lines = token.text.split("\n");
62
+ const formatted = lines.map((line) => " " + chalk.yellow(line)).join("\n");
63
+ return chalk.dim("```" + (token.lang ?? "")) + "\n" + formatted + "\n" + chalk.dim("```") + "\n\n";
64
+ }
65
+ case "blockquote": {
66
+ const inner = (token.tokens ?? []).map((t) => formatToken(t)).join("").trim();
67
+ return inner.split("\n").map((line) => chalk.dim("\u2502 ") + chalk.italic(line)).join("\n") + "\n\n";
68
+ }
69
+ case "list": {
70
+ const items = (token.items ?? []).map((item, i) => {
71
+ const prefix = token.ordered ? chalk.bold(`${i + 1}.`) + " " : chalk.bold("\u2022") + " ";
72
+ const indent = " ".repeat(listDepth);
73
+ const body = (item.tokens ?? []).map((t) => {
74
+ if (t.type === "list") return "\n" + formatToken(t, listDepth + 1);
75
+ return formatToken(t, listDepth);
76
+ }).join("").trim();
77
+ return indent + prefix + body;
78
+ });
79
+ return items.join("\n") + "\n\n";
80
+ }
81
+ case "hr": {
82
+ return chalk.dim("\u2500".repeat(40)) + "\n\n";
83
+ }
84
+ case "link": {
85
+ const label = (token.tokens ?? []).map((t) => formatToken(t)).join("") || token.text;
86
+ if (token.href && token.href !== label) {
87
+ return label + chalk.dim(` (${token.href})`);
88
+ }
89
+ return label;
90
+ }
91
+ case "image": {
92
+ return chalk.dim(`[image: ${token.text}]`);
93
+ }
94
+ case "html": {
95
+ return token.text.replace(/<[^>]+>/g, "");
96
+ }
97
+ case "text": {
98
+ const inner = (token.tokens ?? []).map((t) => formatToken(t)).join("");
99
+ return inner || token.text;
100
+ }
101
+ case "space": {
102
+ return "\n";
103
+ }
104
+ case "escape": {
105
+ return token.text;
106
+ }
107
+ case "table": {
108
+ const sep = chalk.dim(" | ");
109
+ const header = (token.header ?? []).map(
110
+ (cell) => chalk.bold((cell.tokens ?? []).map((t) => formatToken(t)).join(""))
111
+ ).join(sep);
112
+ const divider = chalk.dim("\u2500".repeat(Math.max(header.length - sep.length * ((token.header?.length ?? 1) - 1), 20)));
113
+ const rows = (token.rows ?? []).map(
114
+ (row) => row.map((cell) => (cell.tokens ?? []).map((t) => formatToken(t)).join("")).join(sep)
115
+ );
116
+ return [header, divider, ...rows].join("\n") + "\n\n";
117
+ }
118
+ default: {
119
+ const t = token;
120
+ return t.text ?? t.raw ?? "";
121
+ }
122
+ }
123
+ }
124
+ function markdownToAnsi(content) {
125
+ configureMarked();
126
+ const tokens = marked.lexer(content);
127
+ return tokens.map((t) => formatToken(t)).join("").trimEnd();
128
+ }
129
+
130
+ // src/Markdown.tsx
131
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
132
+ function Markdown({ children }) {
133
+ return /* @__PURE__ */ jsx(Text, { children: markdownToAnsi(children) });
134
+ }
135
+
136
+ // src/app.tsx
137
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
138
+ var SLASH_COMMANDS = [
139
+ { command: "/help", description: "Show available commands" },
140
+ { command: "/resume", description: "Resume a previous session" },
141
+ { command: "/trace", description: "Show recent trace events" },
142
+ { command: "/config", description: "Show redacted configuration" },
143
+ { command: "/skills", description: "List loaded skills" },
144
+ { command: "/clear", description: "Clear screen and reset context" },
145
+ { command: "/exit", description: "Leave chat" }
146
+ ];
147
+ var VOLE_COLOR = "#d9ff33";
148
+ function Spinner({ label }) {
149
+ const { frame } = useAnimation({ interval: 80 });
150
+ const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
151
+ return /* @__PURE__ */ jsxs2(Text2, { color: "yellow", children: [
152
+ frames[frame % frames.length],
153
+ " ",
154
+ label
155
+ ] });
156
+ }
157
+ function WelcomeScreen({ model, sessionId }) {
158
+ return /* @__PURE__ */ jsx2(Box, { flexDirection: "column", marginBottom: 1, gap: 1, children: /* @__PURE__ */ jsxs2(Box, { flexDirection: "row", gap: 3, alignItems: "flex-start", children: [
159
+ /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", children: [
160
+ /* @__PURE__ */ jsx2(Text2, { color: VOLE_COLOR, children: " (\\_/)" }),
161
+ /* @__PURE__ */ jsx2(Text2, { color: VOLE_COLOR, children: " (\u2022\u1D65\u2022)" }),
162
+ /* @__PURE__ */ jsx2(Text2, { color: VOLE_COLOR, children: " /> \\" })
163
+ ] }),
164
+ /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", gap: 1, children: [
165
+ /* @__PURE__ */ jsxs2(Box, { flexDirection: "row", gap: 1, children: [
166
+ /* @__PURE__ */ jsx2(Text2, { color: VOLE_COLOR, bold: true, children: "vole" }),
167
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2014 a capable coding and general-purpose agent" })
168
+ ] }),
169
+ /* @__PURE__ */ jsxs2(Box, { flexDirection: "row", gap: 2, children: [
170
+ /* @__PURE__ */ jsxs2(Box, { flexDirection: "row", gap: 1, children: [
171
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "model" }),
172
+ /* @__PURE__ */ jsx2(Text2, { children: model })
173
+ ] }),
174
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\xB7" }),
175
+ /* @__PURE__ */ jsxs2(Box, { flexDirection: "row", gap: 1, children: [
176
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "session" }),
177
+ /* @__PURE__ */ jsx2(Text2, { color: "blueBright", children: sessionId.slice(-8) })
178
+ ] })
179
+ ] }),
180
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Type /help for commands \xB7 /exit to leave" })
181
+ ] })
182
+ ] }) });
183
+ }
184
+ function CompactHeader({ model, sessionId }) {
185
+ return /* @__PURE__ */ jsxs2(Box, { marginBottom: 1, flexDirection: "row", gap: 2, alignItems: "center", children: [
186
+ /* @__PURE__ */ jsx2(Text2, { color: VOLE_COLOR, bold: true, children: "vole" }),
187
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\xB7" }),
188
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: model }),
189
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\xB7" }),
190
+ /* @__PURE__ */ jsx2(Text2, { color: "blueBright", children: sessionId.slice(-8) }),
191
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\xB7 /help" })
192
+ ] });
193
+ }
194
+ function SessionPicker({
195
+ sessions,
196
+ selectedIndex,
197
+ onSelect,
198
+ onCancel
199
+ }) {
200
+ useInput((_, key) => {
201
+ if (key.escape) {
202
+ onCancel();
203
+ return;
204
+ }
205
+ if (key.return && sessions[selectedIndex] !== void 0) {
206
+ onSelect(sessions[selectedIndex]);
207
+ return;
208
+ }
209
+ });
210
+ if (sessions.length === 0) {
211
+ return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", marginBottom: 1, children: [
212
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No previous sessions found." }),
213
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Esc cancel" })
214
+ ] });
215
+ }
216
+ return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", marginBottom: 1, children: [
217
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, bold: true, children: "Resume session \u2191\u2193 navigate \xB7 Enter select \xB7 Esc cancel" }),
218
+ sessions.map((s, i) => {
219
+ const label = s.title ?? s.id.slice(-12);
220
+ const date = s.updatedAt.slice(0, 16).replace("T", " ");
221
+ const active = i === selectedIndex;
222
+ return /* @__PURE__ */ jsxs2(Box, { gap: 2, children: [
223
+ active ? /* @__PURE__ */ jsx2(Text2, { color: VOLE_COLOR, bold: true, children: " \u25B6" }) : /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " " }),
224
+ /* @__PURE__ */ jsx2(Text2, { ...active ? {} : { dimColor: true }, children: date }),
225
+ active ? /* @__PURE__ */ jsx2(Text2, { color: VOLE_COLOR, children: label }) : /* @__PURE__ */ jsx2(Text2, { children: label })
226
+ ] }, s.id);
227
+ })
228
+ ] });
229
+ }
230
+ function StreamingMessage({ text }) {
231
+ return /* @__PURE__ */ jsxs2(Box, { marginBottom: 1, flexDirection: "column", children: [
232
+ /* @__PURE__ */ jsx2(Text2, { color: "green", bold: true, children: "Assistant" }),
233
+ /* @__PURE__ */ jsxs2(Box, { paddingLeft: 2, children: [
234
+ /* @__PURE__ */ jsx2(Text2, { children: text }),
235
+ /* @__PURE__ */ jsx2(Text2, { color: "blueBright", children: "\u258A" })
236
+ ] })
237
+ ] });
238
+ }
239
+ function ToolProgress({ toolName, input }) {
240
+ const preview = input !== void 0 ? (() => {
241
+ try {
242
+ const s = JSON.stringify(input);
243
+ return s.length > 60 ? s.slice(0, 57) + "\u2026" : s;
244
+ } catch {
245
+ return "";
246
+ }
247
+ })() : "";
248
+ return /* @__PURE__ */ jsxs2(Box, { marginBottom: 1, flexDirection: "column", children: [
249
+ /* @__PURE__ */ jsx2(Spinner, { label: `${toolName}` }),
250
+ preview !== "" && /* @__PURE__ */ jsx2(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: preview }) })
251
+ ] });
252
+ }
253
+ function TodosPanel({ todos }) {
254
+ if (todos.length === 0) return null;
255
+ const done = todos.filter((t) => t.status === "completed").length;
256
+ return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", marginBottom: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [
257
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, bold: true, children: `Todo ${done}/${todos.length}` }),
258
+ todos.map((todo, i) => {
259
+ const icon = todo.status === "completed" ? "\u2713" : todo.status === "in_progress" ? "\u203A" : "\xB7";
260
+ const color = todo.status === "completed" ? "green" : todo.status === "in_progress" ? "yellow" : void 0;
261
+ return /* @__PURE__ */ jsxs2(Box, { children: [
262
+ color !== void 0 ? /* @__PURE__ */ jsxs2(Text2, { color, children: [
263
+ icon,
264
+ " "
265
+ ] }) : /* @__PURE__ */ jsxs2(Text2, { children: [
266
+ icon,
267
+ " "
268
+ ] }),
269
+ /* @__PURE__ */ jsx2(Text2, { dimColor: todo.status === "completed", children: todo.content })
270
+ ] }, i);
271
+ })
272
+ ] });
273
+ }
274
+ function ApprovalPrompt({
275
+ request,
276
+ onApprove,
277
+ onDeny
278
+ }) {
279
+ useInput((inputChar) => {
280
+ if (inputChar === "y" || inputChar === "Y") {
281
+ onApprove();
282
+ } else {
283
+ onDeny();
284
+ }
285
+ });
286
+ const inputPreview = (() => {
287
+ try {
288
+ const s = JSON.stringify(request.call.input, null, 2);
289
+ const lines = s.split("\n");
290
+ return lines.length > 6 ? lines.slice(0, 6).join("\n") + "\n \u2026" : s;
291
+ } catch {
292
+ return "";
293
+ }
294
+ })();
295
+ const riskColor = request.decision.risk === "high" ? "red" : request.decision.risk === "medium" ? "yellow" : "green";
296
+ return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginBottom: 1, children: [
297
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: "\u26A0 Approval Required" }),
298
+ /* @__PURE__ */ jsxs2(Box, { gap: 1, children: [
299
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Tool:" }),
300
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: request.call.name }),
301
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\xB7" }),
302
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Risk:" }),
303
+ /* @__PURE__ */ jsx2(Text2, { color: riskColor, bold: true, children: request.decision.risk })
304
+ ] }),
305
+ inputPreview !== "" && /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: [
306
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Input:" }),
307
+ /* @__PURE__ */ jsx2(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: inputPreview }) })
308
+ ] }),
309
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: request.decision.reason }),
310
+ /* @__PURE__ */ jsx2(Box, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: " y approve any other key deny" }) })
311
+ ] });
312
+ }
313
+ function SuggestionsBox({
314
+ suggestions,
315
+ selectedIndex
316
+ }) {
317
+ if (suggestions.length === 0) return null;
318
+ return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", marginBottom: 1, paddingLeft: 2, children: [
319
+ suggestions.map((s, i) => /* @__PURE__ */ jsxs2(Box, { gap: 2, children: [
320
+ i === selectedIndex ? /* @__PURE__ */ jsx2(Text2, { color: "cyan", bold: true, children: s.command }) : /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: s.command }),
321
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: s.description })
322
+ ] }, s.command)),
323
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Tab \xB7 complete \u2191\u2193 \xB7 select" })
324
+ ] });
325
+ }
326
+ function ChatApp({ config, cliOptions, sessionId }) {
327
+ const { exit } = useApp();
328
+ const { write: writeToStdout } = useStdout();
329
+ const [session, setSession] = useState(null);
330
+ const [loadError, setLoadError] = useState(null);
331
+ const [activeSessionId, setActiveSessionId] = useState(sessionId);
332
+ const [messages, setMessages] = useState([]);
333
+ const [input, setInput] = useState("");
334
+ const [isSending, setIsSending] = useState(false);
335
+ const [inputHistory, setInputHistory] = useState([]);
336
+ const [historyIndex, setHistoryIndex] = useState(-1);
337
+ const [draftInput, setDraftInput] = useState("");
338
+ const [suggestionIndex, setSuggestionIndex] = useState(0);
339
+ const [streamingText, setStreamingText] = useState("");
340
+ const [currentTool, setCurrentTool] = useState(null);
341
+ const [todos, setTodos] = useState([]);
342
+ const [sessionPicker, setSessionPicker] = useState(null);
343
+ const [pendingApproval, setPendingApproval] = useState(null);
344
+ const suggestions = useMemo(() => {
345
+ if (!input.startsWith("/")) return [];
346
+ if (input === "/") return SLASH_COMMANDS;
347
+ return SLASH_COMMANDS.filter(
348
+ (c) => c.command.startsWith(input) && c.command !== input
349
+ );
350
+ }, [input]);
351
+ const showSuggestions = suggestions.length > 0;
352
+ const inkApprovalResolver = useMemo(
353
+ () => ({
354
+ resolve: (request) => new Promise((resolve) => {
355
+ setPendingApproval({
356
+ request,
357
+ resolve: (decision) => {
358
+ setPendingApproval(null);
359
+ resolve(decision);
360
+ }
361
+ });
362
+ })
363
+ }),
364
+ []
365
+ );
366
+ useEffect(() => {
367
+ CliChatSession.createConfigured(config, cliOptions, {
368
+ approvalResolver: inkApprovalResolver,
369
+ preferStreaming: true,
370
+ ...sessionId !== void 0 ? { sessionId } : {}
371
+ }).then((s) => {
372
+ setSession(s);
373
+ setActiveSessionId(s.sessionId);
374
+ }).catch((err) => {
375
+ setLoadError(err instanceof Error ? err.message : "Failed to create session.");
376
+ });
377
+ }, [config, cliOptions, inkApprovalResolver, sessionId]);
378
+ const resetSession = useCallback(async () => {
379
+ session?.close();
380
+ setSession(null);
381
+ setMessages([]);
382
+ setStreamingText("");
383
+ setCurrentTool(null);
384
+ setTodos([]);
385
+ setPendingApproval(null);
386
+ try {
387
+ const s = await CliChatSession.createConfigured(config, cliOptions, {
388
+ approvalResolver: inkApprovalResolver,
389
+ preferStreaming: true
390
+ });
391
+ setSession(s);
392
+ setActiveSessionId(s.sessionId);
393
+ } catch (err) {
394
+ setLoadError(err instanceof Error ? err.message : "Failed to reset session.");
395
+ }
396
+ }, [session, config, cliOptions, inkApprovalResolver]);
397
+ const handleResumeSession = useCallback(async (target) => {
398
+ setSessionPicker(null);
399
+ session?.close();
400
+ setSession(null);
401
+ setMessages([]);
402
+ setStreamingText("");
403
+ setCurrentTool(null);
404
+ setTodos([]);
405
+ setPendingApproval(null);
406
+ try {
407
+ const s = await CliChatSession.createConfigured(config, cliOptions, {
408
+ approvalResolver: inkApprovalResolver,
409
+ preferStreaming: true,
410
+ sessionId: target.id
411
+ });
412
+ const stored = await s.loadMessages();
413
+ const history = stored.flatMap((m) => {
414
+ if (m.role === "user" && m.content) {
415
+ return [{ role: "user", content: m.content }];
416
+ }
417
+ if (m.role === "assistant" && m.content) {
418
+ return [{ role: "assistant", content: m.content }];
419
+ }
420
+ return [];
421
+ });
422
+ setMessages(history);
423
+ setSession(s);
424
+ setActiveSessionId(s.sessionId);
425
+ } catch (err) {
426
+ setLoadError(err instanceof Error ? err.message : "Failed to resume session.");
427
+ }
428
+ }, [session, config, cliOptions, inkApprovalResolver]);
429
+ const handleEvent = useCallback((event) => {
430
+ if (event.type === "token_delta") {
431
+ setStreamingText((prev) => prev + event.delta);
432
+ } else if (event.type === "tool_started") {
433
+ setCurrentTool({ name: event.toolName });
434
+ } else if (event.type === "tool_call_requested") {
435
+ setCurrentTool({ name: event.call.name, input: event.call.input });
436
+ } else if (event.type === "tool_completed") {
437
+ setCurrentTool(null);
438
+ if (event.toolName === "update_todos") return;
439
+ const resultText = renderToolResult(event.result);
440
+ setMessages((prev) => [...prev, { role: "tool_result", toolName: event.toolName, content: resultText, ok: true }]);
441
+ } else if (event.type === "tool_failed") {
442
+ setCurrentTool(null);
443
+ setMessages((prev) => [...prev, { role: "tool_result", toolName: event.toolName, content: event.error.message, ok: false }]);
444
+ } else if (event.type === "todos_updated") {
445
+ setTodos([...event.todos]);
446
+ } else if (event.type === "run_failed") {
447
+ setMessages((prev) => [...prev, { role: "error", content: event.error.message }]);
448
+ }
449
+ }, []);
450
+ const abortControllerRef = useRef2(null);
451
+ const sendMessage = useCallback(
452
+ async (message) => {
453
+ if (session === null || isSending || message.trim() === "") return;
454
+ const trimmed = message.trim();
455
+ const controller = new AbortController();
456
+ abortControllerRef.current = controller;
457
+ setMessages((prev) => [...prev, { role: "user", content: trimmed }]);
458
+ setIsSending(true);
459
+ setStreamingText("");
460
+ setCurrentTool(null);
461
+ try {
462
+ const turn = await session.sendMessage(trimmed, { onEvent: handleEvent, signal: controller.signal });
463
+ setStreamingText("");
464
+ setCurrentTool(null);
465
+ if (turn.assistantText !== "" && !controller.signal.aborted) {
466
+ setMessages((prev) => [...prev, { role: "assistant", content: turn.assistantText }]);
467
+ }
468
+ } finally {
469
+ abortControllerRef.current = null;
470
+ setIsSending(false);
471
+ }
472
+ },
473
+ [session, isSending, handleEvent]
474
+ );
475
+ const handleSlashCommand = useCallback(
476
+ async (command) => {
477
+ if (session === null) return;
478
+ if (command === "/resume") {
479
+ const sessions = await session.listSessions({ limit: 20 });
480
+ const resumable = sessions.filter((s) => s.id !== session.sessionId);
481
+ setSessionPicker({ sessions: resumable, selectedIndex: 0 });
482
+ return;
483
+ }
484
+ if (command === "/clear") {
485
+ writeToStdout("\x1B[2J\x1B[H");
486
+ void resetSession();
487
+ return;
488
+ }
489
+ const lines = await session.runSlashCommand(command);
490
+ setMessages((prev) => [...prev, { role: "slash_result", command, lines }]);
491
+ },
492
+ [session, writeToStdout, resetSession, handleResumeSession]
493
+ );
494
+ const handleChange = useCallback(
495
+ (value) => {
496
+ if (value.includes(" ")) {
497
+ const base = value.replace(/\t/g, "");
498
+ const filtered = base.startsWith("/") ? SLASH_COMMANDS.filter((c) => c.command.startsWith(base) && c.command !== base) : [];
499
+ const target = filtered[suggestionIndex] ?? filtered[0];
500
+ setInput(target !== void 0 ? target.command : base);
501
+ setSuggestionIndex(0);
502
+ setHistoryIndex(-1);
503
+ } else {
504
+ setInput(value);
505
+ setSuggestionIndex(0);
506
+ setHistoryIndex(-1);
507
+ setDraftInput(value);
508
+ }
509
+ },
510
+ [suggestionIndex]
511
+ );
512
+ useInput(
513
+ (_, key) => {
514
+ if (sessionPicker !== null) {
515
+ if (key.upArrow) {
516
+ setSessionPicker((p) => p && { ...p, selectedIndex: Math.max(0, p.selectedIndex - 1) });
517
+ } else if (key.downArrow) {
518
+ setSessionPicker((p) => p && { ...p, selectedIndex: Math.min(p.sessions.length - 1, p.selectedIndex + 1) });
519
+ }
520
+ return;
521
+ }
522
+ if (key.upArrow) {
523
+ if (showSuggestions) {
524
+ setSuggestionIndex((i) => Math.max(0, i - 1));
525
+ } else if (historyIndex < inputHistory.length - 1) {
526
+ const newIndex = historyIndex + 1;
527
+ setHistoryIndex(newIndex);
528
+ setInput(inputHistory[newIndex] ?? "");
529
+ }
530
+ } else if (key.downArrow) {
531
+ if (showSuggestions) {
532
+ setSuggestionIndex((i) => Math.min(suggestions.length - 1, i + 1));
533
+ } else if (historyIndex > 0) {
534
+ const newIndex = historyIndex - 1;
535
+ setHistoryIndex(newIndex);
536
+ setInput(inputHistory[newIndex] ?? "");
537
+ } else if (historyIndex === 0) {
538
+ setHistoryIndex(-1);
539
+ setInput(draftInput);
540
+ }
541
+ }
542
+ },
543
+ { isActive: !isSending && pendingApproval === null }
544
+ );
545
+ useInput(
546
+ (_, key) => {
547
+ if (key.escape) {
548
+ abortControllerRef.current?.abort();
549
+ setStreamingText("");
550
+ setCurrentTool(null);
551
+ }
552
+ },
553
+ { isActive: isSending }
554
+ );
555
+ useInput(
556
+ (inputChar, key) => {
557
+ if (key.ctrl && inputChar === "c") exit();
558
+ },
559
+ { isActive: true }
560
+ );
561
+ const handleSubmit = useCallback(
562
+ (value) => {
563
+ if (isSending || pendingApproval !== null) return;
564
+ const trimmed = value.trim();
565
+ if (trimmed === "") return;
566
+ setInput("");
567
+ setHistoryIndex(-1);
568
+ setSuggestionIndex(0);
569
+ if (trimmed === "/exit") {
570
+ exit();
571
+ return;
572
+ }
573
+ if (trimmed.startsWith("/")) {
574
+ void handleSlashCommand(trimmed);
575
+ return;
576
+ }
577
+ setInputHistory((prev) => prev[0] === trimmed ? prev : [trimmed, ...prev.slice(0, 49)]);
578
+ void sendMessage(trimmed);
579
+ },
580
+ [isSending, pendingApproval, sendMessage, exit, handleSlashCommand]
581
+ );
582
+ if (loadError !== null) {
583
+ return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", padding: 1, children: [
584
+ /* @__PURE__ */ jsx2(Text2, { color: "red", bold: true, children: "Error" }),
585
+ /* @__PURE__ */ jsx2(Text2, { children: loadError })
586
+ ] });
587
+ }
588
+ if (session === null) {
589
+ return /* @__PURE__ */ jsx2(Box, { padding: 1, children: /* @__PURE__ */ jsx2(Spinner, { label: "Starting Vole\u2026" }) });
590
+ }
591
+ const sidLabel = activeSessionId ?? "\u2026";
592
+ const modelLabel = `${config.model.provider}/${config.model.model}`;
593
+ const hasMessages = messages.length > 0 || streamingText !== "" || currentTool !== null || isSending;
594
+ return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", children: [
595
+ hasMessages ? /* @__PURE__ */ jsx2(CompactHeader, { model: modelLabel, sessionId: sidLabel }) : /* @__PURE__ */ jsx2(WelcomeScreen, { model: modelLabel, sessionId: sidLabel }),
596
+ /* @__PURE__ */ jsx2(Static, { items: messages, children: (msg, i) => {
597
+ if (msg.role === "user") return /* @__PURE__ */ jsxs2(Box, { marginBottom: 1, children: [
598
+ /* @__PURE__ */ jsx2(Text2, { color: "cyan", bold: true, children: "You " }),
599
+ /* @__PURE__ */ jsx2(Text2, { children: msg.content })
600
+ ] }, i);
601
+ if (msg.role === "tool_result") return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", marginBottom: 1, paddingLeft: 2, children: [
602
+ /* @__PURE__ */ jsxs2(Box, { gap: 1, children: [
603
+ /* @__PURE__ */ jsx2(Text2, { color: msg.ok ? "green" : "red", children: msg.ok ? "\u2713" : "\u2717" }),
604
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, bold: true, children: msg.toolName })
605
+ ] }),
606
+ /* @__PURE__ */ jsx2(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
607
+ msg.content.slice(0, 200),
608
+ msg.content.length > 200 ? " \u2026" : ""
609
+ ] }) })
610
+ ] }, i);
611
+ if (msg.role === "error") return /* @__PURE__ */ jsxs2(Box, { marginBottom: 1, borderStyle: "single", borderColor: "red", paddingX: 1, children: [
612
+ /* @__PURE__ */ jsx2(Text2, { color: "red", bold: true, children: "\u2717 " }),
613
+ /* @__PURE__ */ jsx2(Text2, { color: "red", children: msg.content })
614
+ ] }, i);
615
+ if (msg.role === "slash_result") return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", marginBottom: 1, paddingLeft: 2, children: [
616
+ /* @__PURE__ */ jsx2(Text2, { color: "blue", dimColor: true, children: msg.command }),
617
+ msg.lines.map((line, j) => /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: line }, j))
618
+ ] }, i);
619
+ return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", marginBottom: 1, children: [
620
+ /* @__PURE__ */ jsx2(Text2, { color: "green", bold: true, children: "Assistant" }),
621
+ /* @__PURE__ */ jsx2(Box, { paddingLeft: 2, flexDirection: "column", children: /* @__PURE__ */ jsx2(Markdown, { children: msg.content }) })
622
+ ] }, i);
623
+ } }),
624
+ streamingText !== "" && /* @__PURE__ */ jsx2(StreamingMessage, { text: streamingText }),
625
+ currentTool !== null && /* @__PURE__ */ jsx2(ToolProgress, { toolName: currentTool.name, input: currentTool.input }),
626
+ /* @__PURE__ */ jsx2(TodosPanel, { todos }),
627
+ pendingApproval !== null && /* @__PURE__ */ jsx2(
628
+ ApprovalPrompt,
629
+ {
630
+ request: pendingApproval.request,
631
+ onApprove: () => pendingApproval.resolve({ approved: true, reason: "Approved from CLI." }),
632
+ onDeny: () => pendingApproval.resolve({ approved: false, reason: "Denied from CLI." })
633
+ }
634
+ ),
635
+ sessionPicker !== null && /* @__PURE__ */ jsx2(
636
+ SessionPicker,
637
+ {
638
+ sessions: sessionPicker.sessions,
639
+ selectedIndex: sessionPicker.selectedIndex,
640
+ onSelect: (s) => void handleResumeSession(s),
641
+ onCancel: () => setSessionPicker(null)
642
+ }
643
+ ),
644
+ !isSending && pendingApproval === null && sessionPicker === null && /* @__PURE__ */ jsx2(SuggestionsBox, { suggestions, selectedIndex: suggestionIndex }),
645
+ !isSending && pendingApproval === null && sessionPicker === null && /* @__PURE__ */ jsxs2(Box, { gap: 1, children: [
646
+ /* @__PURE__ */ jsx2(Text2, { color: "cyan", bold: true, children: "\u203A" }),
647
+ /* @__PURE__ */ jsx2(
648
+ TextInput,
649
+ {
650
+ value: input,
651
+ onChange: handleChange,
652
+ onSubmit: handleSubmit,
653
+ focus: pendingApproval === null && !isSending
654
+ }
655
+ )
656
+ ] }),
657
+ isSending && pendingApproval === null && /* @__PURE__ */ jsxs2(Box, { gap: 2, children: [
658
+ /* @__PURE__ */ jsx2(Spinner, { label: "Thinking\u2026" }),
659
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Esc to stop" })
660
+ ] })
661
+ ] });
662
+ }
663
+ async function runInkChat({ args, env, sessionsDirectory }) {
664
+ let config;
665
+ try {
666
+ config = loadConfig({ env });
667
+ } catch (err) {
668
+ process.stderr.write(
669
+ `Configuration error: ${err instanceof Error ? err.message : String(err)}
670
+ `
671
+ );
672
+ process.exitCode = 1;
673
+ return;
674
+ }
675
+ if (config.secrets.apiKey === void 0) {
676
+ process.stderr.write(
677
+ "Missing VOLE_API_KEY or OPENROUTER_API_KEY. Set one to start `vole chat`, or use `vole chat --fake-interactive` for local learning.\n"
678
+ );
679
+ process.exitCode = 1;
680
+ return;
681
+ }
682
+ const sessionIndex = args.indexOf("--session");
683
+ const sessionId = sessionIndex !== -1 && args[sessionIndex + 1] !== void 0 ? args[sessionIndex + 1] : void 0;
684
+ const cliOptions = {
685
+ env,
686
+ ...sessionsDirectory !== void 0 ? { sessionsDirectory } : {}
687
+ };
688
+ const { waitUntilExit } = render(
689
+ /* @__PURE__ */ jsx2(
690
+ ChatApp,
691
+ {
692
+ config,
693
+ cliOptions,
694
+ ...sessionId !== void 0 ? { sessionId } : {}
695
+ }
696
+ )
697
+ );
698
+ await waitUntilExit();
699
+ }
700
+ export {
701
+ runInkChat
702
+ };