@nodetool-ai/cli-chat 0.0.0-stage → 0.8.9-bootstrap.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.
Files changed (107) hide show
  1. package/README.md +13 -2
  2. package/dist/.nodetool-build-stamp +1 -0
  3. package/dist/ExecutionTree.d.ts +20 -0
  4. package/dist/ExecutionTree.d.ts.map +1 -0
  5. package/dist/ExecutionTree.js +100 -0
  6. package/dist/ExecutionTree.js.map +1 -0
  7. package/dist/agent-toolbelt.d.ts +15 -0
  8. package/dist/agent-toolbelt.d.ts.map +1 -0
  9. package/dist/agent-toolbelt.js +28 -0
  10. package/dist/agent-toolbelt.js.map +1 -0
  11. package/dist/app.d.ts +40 -0
  12. package/dist/app.d.ts.map +1 -0
  13. package/dist/app.js +1070 -0
  14. package/dist/app.js.map +1 -0
  15. package/dist/bash-tool.d.ts +13 -0
  16. package/dist/bash-tool.d.ts.map +1 -0
  17. package/dist/bash-tool.js +85 -0
  18. package/dist/bash-tool.js.map +1 -0
  19. package/dist/chat-codeact.d.ts +93 -0
  20. package/dist/chat-codeact.d.ts.map +1 -0
  21. package/dist/chat-codeact.js +179 -0
  22. package/dist/chat-codeact.js.map +1 -0
  23. package/dist/chat-context.d.ts +18 -0
  24. package/dist/chat-context.d.ts.map +1 -0
  25. package/dist/chat-context.js +32 -0
  26. package/dist/chat-context.js.map +1 -0
  27. package/dist/chat-entry.d.ts +13 -0
  28. package/dist/chat-entry.d.ts.map +1 -0
  29. package/dist/chat-entry.js +233 -0
  30. package/dist/chat-entry.js.map +1 -0
  31. package/dist/chat-media.d.ts +3 -0
  32. package/dist/chat-media.d.ts.map +1 -0
  33. package/dist/chat-media.js +46 -0
  34. package/dist/chat-media.js.map +1 -0
  35. package/dist/chat-prompts.d.ts +20 -0
  36. package/dist/chat-prompts.d.ts.map +1 -0
  37. package/dist/chat-prompts.js +42 -0
  38. package/dist/chat-prompts.js.map +1 -0
  39. package/dist/chat-sessions.d.ts +25 -0
  40. package/dist/chat-sessions.d.ts.map +1 -0
  41. package/dist/chat-sessions.js +153 -0
  42. package/dist/chat-sessions.js.map +1 -0
  43. package/dist/commands/agent.d.ts +33 -0
  44. package/dist/commands/agent.d.ts.map +1 -0
  45. package/dist/commands/agent.js +516 -0
  46. package/dist/commands/agent.js.map +1 -0
  47. package/dist/commands/tools.d.ts +11 -0
  48. package/dist/commands/tools.d.ts.map +1 -0
  49. package/dist/commands/tools.js +206 -0
  50. package/dist/commands/tools.js.map +1 -0
  51. package/dist/diagnose.d.ts +139 -0
  52. package/dist/diagnose.d.ts.map +1 -0
  53. package/dist/diagnose.js +435 -0
  54. package/dist/diagnose.js.map +1 -0
  55. package/dist/index.d.ts +14 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +40 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/markdown.d.ts +9 -0
  60. package/dist/markdown.d.ts.map +1 -0
  61. package/dist/markdown.js +53 -0
  62. package/dist/markdown.js.map +1 -0
  63. package/dist/nodetool.d.ts +3 -0
  64. package/dist/nodetool.d.ts.map +1 -0
  65. package/dist/nodetool.js +4 -0
  66. package/dist/nodetool.js.map +1 -0
  67. package/dist/permission-gate.d.ts +55 -0
  68. package/dist/permission-gate.d.ts.map +1 -0
  69. package/dist/permission-gate.js +118 -0
  70. package/dist/permission-gate.js.map +1 -0
  71. package/dist/readline-input.d.ts +23 -0
  72. package/dist/readline-input.d.ts.map +1 -0
  73. package/dist/readline-input.js +210 -0
  74. package/dist/readline-input.js.map +1 -0
  75. package/dist/settings.d.ts +19 -0
  76. package/dist/settings.d.ts.map +1 -0
  77. package/dist/settings.js +146 -0
  78. package/dist/settings.js.map +1 -0
  79. package/dist/stdin.d.ts +52 -0
  80. package/dist/stdin.d.ts.map +1 -0
  81. package/dist/stdin.js +363 -0
  82. package/dist/stdin.js.map +1 -0
  83. package/dist/terminal-clipboard.d.ts +3 -0
  84. package/dist/terminal-clipboard.d.ts.map +1 -0
  85. package/dist/terminal-clipboard.js +24 -0
  86. package/dist/terminal-clipboard.js.map +1 -0
  87. package/dist/terminal-mouse.d.ts +24 -0
  88. package/dist/terminal-mouse.d.ts.map +1 -0
  89. package/dist/terminal-mouse.js +84 -0
  90. package/dist/terminal-mouse.js.map +1 -0
  91. package/dist/terminal-screen.d.ts +41 -0
  92. package/dist/terminal-screen.d.ts.map +1 -0
  93. package/dist/terminal-screen.js +228 -0
  94. package/dist/terminal-screen.js.map +1 -0
  95. package/dist/tool-format.d.ts +54 -0
  96. package/dist/tool-format.d.ts.map +1 -0
  97. package/dist/tool-format.js +360 -0
  98. package/dist/tool-format.js.map +1 -0
  99. package/dist/useExecutionState.d.ts +44 -0
  100. package/dist/useExecutionState.d.ts.map +1 -0
  101. package/dist/useExecutionState.js +285 -0
  102. package/dist/useExecutionState.js.map +1 -0
  103. package/dist/websocket-client.d.ts +159 -0
  104. package/dist/websocket-client.d.ts.map +1 -0
  105. package/dist/websocket-client.js +677 -0
  106. package/dist/websocket-client.js.map +1 -0
  107. package/package.json +73 -4
package/dist/app.js ADDED
@@ -0,0 +1,1070 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect, useRef, useCallback } from "react";
3
+ import { Box, Text, useApp, useInput } from "ink";
4
+ import Spinner from "ink-spinner";
5
+ import { writeFile } from "node:fs/promises";
6
+ import { resolve } from "node:path";
7
+ import { PERMISSION_GATE_CONTEXT_KEY, RUN_BUDGET_CONTEXT_KEY, estimatePromptTokens } from "@nodetool-ai/runtime";
8
+ import { processChat } from "@nodetool-ai/chat";
9
+ import { availableProviders, createProvider, listConfiguredLanguageModels } from "@nodetool-ai/cli-providers";
10
+ import { WebSocketChatClient } from "./websocket-client.js";
11
+ import { budgetStopReason, budgetSummaryLine, createCliRunBudget } from "@nodetool-ai/cli-execution/run-budget";
12
+ import { renderMarkdown } from "./markdown.js";
13
+ import { friendlyToolName, formatToolResult, isFormattedTool } from "./tool-format.js";
14
+ import { saveSettings } from "./settings.js";
15
+ import { buildCliToolbelt } from "./agent-toolbelt.js";
16
+ import { BashTool } from "./bash-tool.js";
17
+ import { applySystemPrompt, buildCliAgentBelt, createCliCodeActTurn, loadCliSkillCatalog } from "./chat-codeact.js";
18
+ import { createChatContext } from "./chat-context.js";
19
+ import { isNumber, isRecord, isString } from "@nodetool-ai/cli-common/predicates";
20
+ import { parsePermissionMode } from "./permission-gate.js";
21
+ import ReadlineInput from "./readline-input.js";
22
+ import { Transcript, terminalLines, terminalText, useTerminalSize } from "./terminal-screen.js";
23
+ import { ChatSessionStore, newSessionId, exportTranscript } from "./chat-sessions.js";
24
+ import { ChatPrompts } from "./chat-prompts.js";
25
+ import { attachmentLines } from "./chat-media.js";
26
+ export const CHAT_COMMANDS = {
27
+ "/help": "Commands and keyboard shortcuts",
28
+ "/new": "Start a new conversation",
29
+ "/clear": "Clear the screen, keep conversation context",
30
+ "/compact": "Summarize retained context",
31
+ "/model": "Choose a model",
32
+ "/agent": "Inspect sub-agent threads: /agent [id|main]",
33
+ "/mode": "Permissions: default, auto, plan",
34
+ "/sessions": "Browse saved conversations",
35
+ "/resume": "Resume a session: /resume <id>",
36
+ "/export": "Save transcript: /export <path.md>",
37
+ "/tools": "Show available tools",
38
+ "/details": "Toggle tool arguments, code and diffs",
39
+ "/exit": "Save and quit",
40
+ "/quit": "Save and quit"
41
+ };
42
+ const APPROVAL_CHOICES = [
43
+ { key: "y", label: "Allow once", value: "allow" },
44
+ { key: "a", label: "Allow for chat", value: "allow_for_chat" },
45
+ { key: "n", label: "Deny", value: "deny" }
46
+ ];
47
+ function parseSubAgentEventTags(message) {
48
+ if (!isRecord(message) || !isString(message.parent_tool_call_id)) {
49
+ return null;
50
+ }
51
+ return {
52
+ parentId: message.parent_tool_call_id,
53
+ depth: isNumber(message.subtask_depth) ? message.subtask_depth : 1
54
+ };
55
+ }
56
+ const DELEGATION_TOOLS = new Set([
57
+ "run_subtask",
58
+ "start_subtask",
59
+ "run_search"
60
+ ]);
61
+ function errorText(error) {
62
+ return error instanceof Error ? error.message : String(error);
63
+ }
64
+ function displayResult(name, args, result) {
65
+ return isFormattedTool(name)
66
+ ? formatToolResult(name, args, result)
67
+ : isString(result)
68
+ ? result
69
+ : (JSON.stringify(result, null, 2) ?? "Done");
70
+ }
71
+ export function App({ initialProvider, initialModel, enabledTools, workspaceDir, wsUrl, registry, agentProviders, costCap, timeout, permissionMode = "default", enableReadOnlySearch = true, resume, sessionStore, mouseEvents }) {
72
+ const { exit } = useApp();
73
+ const { columns, rows } = useTerminalSize();
74
+ const [provider, setProvider] = useState(initialProvider);
75
+ const [model, setModel] = useState(initialModel);
76
+ const [mode, setMode] = useState(permissionMode);
77
+ const [messages, setMessages] = useState([]);
78
+ const [clearedCount, setClearedCount] = useState(0);
79
+ const [input, setInput] = useState("");
80
+ const [busy, setBusy] = useState(false);
81
+ const [status, setStatus] = useState("Ready");
82
+ const [live, setLive] = useState("");
83
+ const [details, setDetails] = useState(false);
84
+ const [prompt, setPrompt] = useState(null);
85
+ const [completionIndex, setCompletionIndex] = useState(0);
86
+ const [models, setModels] = useState([]);
87
+ const [configuredProviders, setConfiguredProviders] = useState(() => new Set(availableProviders()));
88
+ const [agentThreads, setAgentThreads] = useState({});
89
+ const [selectedAgentId, setSelectedAgentId] = useState(null);
90
+ const [sessions, setSessions] = useState([]);
91
+ const [todos, setTodos] = useState([]);
92
+ const [tasks, setTasks] = useState({});
93
+ const [activeTools, setActiveTools] = useState({});
94
+ const [elapsed, setElapsed] = useState(0);
95
+ const [usage, setUsage] = useState("");
96
+ const [contextTokens, setContextTokens] = useState(null);
97
+ const refreshContextTokens = useCallback(() => {
98
+ setContextTokens(wsUrl ? null : estimatePromptTokens(history.current));
99
+ }, [wsUrl]);
100
+ const [connection, setConnection] = useState(wsUrl ? "Connecting" : "Local");
101
+ const [sessionId, setSessionId] = useState(newSessionId);
102
+ const [historyIndex, setHistoryIndex] = useState(-1);
103
+ const [inputHistory, setInputHistory] = useState([]);
104
+ const [draft, setDraft] = useState("");
105
+ const store = useRef(sessionStore ?? new ChatSessionStore());
106
+ const prompts = useRef(new ChatPrompts(setPrompt));
107
+ const controller = useRef(null);
108
+ const client = useRef(null);
109
+ const history = useRef([]);
110
+ const transcript = useRef([]);
111
+ const agentThreadsRef = useRef({});
112
+ const stream = useRef("");
113
+ const thread = useRef(newSessionId());
114
+ const providerSession = useRef(null);
115
+ const sessionAllow = useRef(new Set());
116
+ const active = useRef(false);
117
+ const mounted = useRef(true);
118
+ const scrollGeneration = useRef(0);
119
+ const [scrollKey, setScrollKey] = useState("0");
120
+ const updateMessages = useCallback((next) => {
121
+ transcript.current = next;
122
+ setMessages(next);
123
+ }, []);
124
+ function updateAgentThread(id, update) {
125
+ const next = {
126
+ ...agentThreadsRef.current,
127
+ [id]: update(agentThreadsRef.current[id])
128
+ };
129
+ agentThreadsRef.current = next;
130
+ setAgentThreads(next);
131
+ }
132
+ function startAgentThread(id, name, args, parentId, depth = 1) {
133
+ if (!DELEGATION_TOOLS.has(name) || agentThreadsRef.current[id]) {
134
+ return;
135
+ }
136
+ const title = [args.description, args.query, args.task].find(isString);
137
+ const thread = {
138
+ id,
139
+ title: title || friendlyToolName(name),
140
+ depth,
141
+ status: "running",
142
+ messages: []
143
+ };
144
+ if (parentId) {
145
+ Object.assign(thread, { parentId });
146
+ }
147
+ updateAgentThread(id, () => thread);
148
+ }
149
+ function appendAgentMessage(id, message, status) {
150
+ updateAgentThread(id, (thread) => {
151
+ const next = {
152
+ ...(thread ?? {
153
+ id,
154
+ title: "Sub-agent",
155
+ depth: 1,
156
+ status: "running",
157
+ messages: []
158
+ }),
159
+ messages: [...(thread?.messages ?? []), message]
160
+ };
161
+ if (status) {
162
+ Object.assign(next, { status });
163
+ }
164
+ return next;
165
+ });
166
+ }
167
+ function agentEvent(message) {
168
+ const tags = parseSubAgentEventTags(message);
169
+ if (!tags) {
170
+ return;
171
+ }
172
+ const { parentId, depth } = tags;
173
+ if (message.type === "tool_call_update" &&
174
+ message.tool_call_id &&
175
+ DELEGATION_TOOLS.has(message.name)) {
176
+ startAgentThread(message.tool_call_id, message.name, message.args, parentId, depth + 1);
177
+ }
178
+ let content;
179
+ let role = "system";
180
+ let status;
181
+ if (message.type === "chunk" && isString(message.content)) {
182
+ if (!message.thinking && message.content) {
183
+ content = message.content;
184
+ role = "assistant";
185
+ }
186
+ }
187
+ else if (message.type === "tool_call_update") {
188
+ content = `${friendlyToolName(message.name)}\n${JSON.stringify(message.args, null, 2)}`;
189
+ role = "tool";
190
+ }
191
+ else if (message.type === "tool_result_update") {
192
+ content = displayResult(message.name ?? "tool", undefined, message.result);
193
+ role = "tool";
194
+ }
195
+ else if (message.type === "planning_update") {
196
+ content = message.content ?? `Planning: ${message.status}`;
197
+ }
198
+ else if (message.type === "task_update") {
199
+ content = `${message.task.title ?? message.task.id}: ${message.event}`;
200
+ }
201
+ else if (message.type === "step_result") {
202
+ content = message.error
203
+ ? `Error: ${message.error}`
204
+ : displayResult("result", undefined, message.result);
205
+ if (message.is_task_result) {
206
+ status = message.error ? "failed" : "completed";
207
+ }
208
+ }
209
+ else if (message.type === "error") {
210
+ content = `Error: ${message.message}`;
211
+ status = "failed";
212
+ }
213
+ if (content) {
214
+ appendAgentMessage(parentId, { id: newSessionId(), role, content }, status);
215
+ }
216
+ }
217
+ function add(role, content, toolName, toolArgs) {
218
+ const message = {
219
+ id: newSessionId(),
220
+ role,
221
+ content,
222
+ toolName,
223
+ toolArgs
224
+ };
225
+ updateMessages([...transcript.current, message]);
226
+ if (role === "assistant") {
227
+ void renderMarkdown(terminalText(content))
228
+ .then((rendered) => {
229
+ if (mounted.current) {
230
+ updateMessages(transcript.current.map((item) => item.id === message.id ? { ...item, rendered } : item));
231
+ }
232
+ })
233
+ .catch(() => {
234
+ /* Raw content stays visible when formatting fails. */
235
+ });
236
+ }
237
+ }
238
+ function flush() {
239
+ if (stream.current.trim()) {
240
+ add("assistant", stream.current);
241
+ }
242
+ stream.current = "";
243
+ setLive("");
244
+ }
245
+ function append(text) {
246
+ stream.current += text;
247
+ setLive(stream.current);
248
+ }
249
+ const latest = useCallback(() => {
250
+ setScrollKey(String(++scrollGeneration.current));
251
+ }, []);
252
+ useEffect(() => {
253
+ mounted.current = true;
254
+ return () => {
255
+ mounted.current = false;
256
+ controller.current?.abort();
257
+ };
258
+ }, []);
259
+ useEffect(() => {
260
+ const abort = new AbortController();
261
+ setModels([]);
262
+ if (!wsUrl) {
263
+ void listConfiguredLanguageModels()
264
+ .then((catalog) => {
265
+ if (!abort.signal.aborted) {
266
+ setConfiguredProviders(new Set(catalog.providers));
267
+ setModels(catalog.models);
268
+ }
269
+ })
270
+ .catch(() => {
271
+ /* Explicit provider/model specs remain available. */
272
+ });
273
+ }
274
+ return () => abort.abort();
275
+ }, [wsUrl]);
276
+ useEffect(() => {
277
+ if (!wsUrl) {
278
+ return;
279
+ }
280
+ const socket = new WebSocketChatClient(wsUrl);
281
+ client.current = socket;
282
+ let cancelled = false;
283
+ void socket
284
+ .connect()
285
+ .then(() => {
286
+ if (!cancelled) {
287
+ setConnection("Connected");
288
+ }
289
+ })
290
+ .catch((error) => {
291
+ if (!cancelled) {
292
+ setConnection("Disconnected");
293
+ setStatus(`Connection failed: ${errorText(error)}`);
294
+ }
295
+ });
296
+ return () => {
297
+ cancelled = true;
298
+ socket.disconnect();
299
+ client.current = null;
300
+ };
301
+ }, [wsUrl]);
302
+ useEffect(() => {
303
+ if (!busy) {
304
+ return;
305
+ }
306
+ const started = Date.now();
307
+ setElapsed(0);
308
+ const timer = setInterval(() => setElapsed(Math.floor((Date.now() - started) / 1000)), 1000);
309
+ return () => clearInterval(timer);
310
+ }, [busy]);
311
+ const restore = useCallback((session) => {
312
+ history.current = session.history;
313
+ refreshContextTokens();
314
+ providerSession.current = session.providerSession ?? null;
315
+ thread.current = session.threadId;
316
+ setSessionId(session.id);
317
+ setProvider(session.provider);
318
+ setModel(session.model);
319
+ updateMessages(session.messages);
320
+ setClearedCount(0);
321
+ setInputHistory(session.messages
322
+ .filter((item) => item.role === "user")
323
+ .map((item) => item.content)
324
+ .reverse());
325
+ sessionAllow.current.clear();
326
+ setTodos([]);
327
+ setTasks({});
328
+ setUsage("");
329
+ latest();
330
+ setStatus(`Resumed ${session.title}`);
331
+ }, [refreshContextTokens, updateMessages, latest]);
332
+ useEffect(() => {
333
+ if (!resume) {
334
+ return;
335
+ }
336
+ let cancelled = false;
337
+ active.current = true;
338
+ setBusy(true);
339
+ void store.current
340
+ .list(workspaceDir, wsUrl)
341
+ .then((saved) => {
342
+ if (cancelled) {
343
+ return;
344
+ }
345
+ const selected = resume === true ? saved[0] : saved.find((item) => item.id === resume);
346
+ if (!selected) {
347
+ throw new Error("No matching saved session in this workspace.");
348
+ }
349
+ restore(selected);
350
+ })
351
+ .catch((error) => {
352
+ if (!cancelled) {
353
+ setStatus(errorText(error));
354
+ }
355
+ })
356
+ .finally(() => {
357
+ if (!cancelled) {
358
+ active.current = false;
359
+ setBusy(false);
360
+ }
361
+ });
362
+ return () => {
363
+ cancelled = true;
364
+ };
365
+ }, [resume, workspaceDir, wsUrl, restore]);
366
+ async function persist() {
367
+ if (!transcript.current.length) {
368
+ return;
369
+ }
370
+ await store.current.save({
371
+ version: 1,
372
+ id: sessionId,
373
+ threadId: thread.current,
374
+ title: transcript.current
375
+ .find((item) => item.role === "user")
376
+ ?.content.slice(0, 80) ?? "Conversation",
377
+ updatedAt: new Date().toISOString(),
378
+ workspace: resolve(workspaceDir),
379
+ server: wsUrl,
380
+ provider,
381
+ model,
382
+ history: history.current,
383
+ messages: transcript.current,
384
+ providerSession: providerSession.current
385
+ });
386
+ }
387
+ async function quit() {
388
+ try {
389
+ await persist();
390
+ await saveSettings({ provider, model });
391
+ exit();
392
+ }
393
+ catch (error) {
394
+ setStatus(`Could not save session: ${errorText(error)}. Try /export.`);
395
+ }
396
+ }
397
+ function tools() {
398
+ const belt = [
399
+ ...buildCliToolbelt(agentProviders ?? {}, registry),
400
+ new BashTool(workspaceDir)
401
+ ];
402
+ return enabledTools
403
+ ? belt.filter((tool) => enabledTools.includes(tool.name))
404
+ : belt;
405
+ }
406
+ function processEvent(message) {
407
+ agentEvent(message);
408
+ if (message.type === "todo_update") {
409
+ setTodos(message.todos);
410
+ }
411
+ else if (message.type === "task_update" && message.task.id) {
412
+ const id = message.task.id;
413
+ setTasks((previous) => ({
414
+ ...previous,
415
+ [id]: `${message.task.title ?? id}: ${message.event}`
416
+ }));
417
+ }
418
+ else if (message.type === "planning_update") {
419
+ setStatus(message.content ?? `Planning: ${message.status}`);
420
+ }
421
+ else if (message.type === "output_update") {
422
+ add("system", `Output ${message.node_id}: ${displayResult("output", undefined, message.value)}`);
423
+ }
424
+ else if (message.type === "prediction") {
425
+ setStatus(`Generation: ${message.status ?? "running"}`);
426
+ }
427
+ else if (message.type === "chunk" &&
428
+ isString(message.content) &&
429
+ !message.thinking) {
430
+ if (message.parent_tool_call_id || message.subtask_depth) {
431
+ setStatus(`Subtask: ${message.content.slice(-100)}`);
432
+ }
433
+ }
434
+ }
435
+ function startTool(id, name, args) {
436
+ flush();
437
+ startAgentThread(id, name, args);
438
+ setActiveTools((previous) => ({ ...previous, [id]: { name, args } }));
439
+ setStatus(friendlyToolName(name));
440
+ }
441
+ function finishTool(id, name, args, result) {
442
+ if (DELEGATION_TOOLS.has(name) && name !== "start_subtask") {
443
+ const failed = isRecord(result) && isString(result.error);
444
+ updateAgentThread(id, (thread) => ({
445
+ ...(thread ?? {
446
+ id,
447
+ title: friendlyToolName(name),
448
+ depth: 1,
449
+ messages: []
450
+ }),
451
+ status: failed ? "failed" : "completed"
452
+ }));
453
+ }
454
+ add("tool", displayResult(name, args, result), name, args);
455
+ setActiveTools((previous) => {
456
+ const next = { ...previous };
457
+ delete next[id];
458
+ return next;
459
+ });
460
+ setStatus("Thinking");
461
+ }
462
+ async function remoteEvent(event, socket, signal) {
463
+ if (event.type === "chunk") {
464
+ append(event.content);
465
+ setStatus("Responding");
466
+ }
467
+ else if (event.type === "assistant_message") {
468
+ if (event.text) {
469
+ append(event.text);
470
+ }
471
+ const attachments = attachmentLines(event.content);
472
+ if (attachments.length) {
473
+ flush();
474
+ add("system", attachments.join("\n"));
475
+ }
476
+ }
477
+ else if (event.type === "processing") {
478
+ processEvent(event.message);
479
+ }
480
+ else if (event.type === "tool_approval_request") {
481
+ const answer = await prompts.current.ask({
482
+ title: `Approve ${event.toolName}`,
483
+ body: `${event.description || event.message}\n\n${JSON.stringify(event.args, null, 2)}`,
484
+ choices: APPROVAL_CHOICES
485
+ }, signal);
486
+ socket.respondToolApproval(event.approvalId, answer === "allow" || answer === "allow_for_chat" ? answer : "deny");
487
+ }
488
+ else if (event.type === "plan_approval_request") {
489
+ const answer = await prompts.current.ask({
490
+ title: event.plan.title,
491
+ body: event.plan.tasks
492
+ .map((task) => `${task.title}\n${task.steps.map((step) => ` ${step.instructions}`).join("\n")}`)
493
+ .join("\n\n"),
494
+ choices: [
495
+ { key: "y", label: "Execute plan", value: "approve" },
496
+ { key: "n", label: "Reject", value: "reject" }
497
+ ]
498
+ }, signal);
499
+ socket.respondPlanApproval(event.approvalId, answer === "approve" ? "approve" : "reject");
500
+ }
501
+ else if (event.type === "secret_request") {
502
+ const answer = await prompts.current.ask({
503
+ title: `Configure ${event.key}`,
504
+ body: `${event.description}\n${event.reason}\nConfigure this secret on the connected server, then continue.\n${event.helpUrl ?? ""}`,
505
+ choices: [
506
+ { key: "y", label: "Configured, retry", value: "provided" },
507
+ { key: "n", label: "Cancel", value: "cancelled" }
508
+ ]
509
+ }, signal);
510
+ socket.respondSecretRequest(event.approvalId, answer === "provided" ? "saved" : "declined");
511
+ }
512
+ else if (event.type === "client_tool_call") {
513
+ socket.respondToolResult(event.id, event.threadId, {
514
+ error: `The terminal does not provide browser tool ${event.name}. Use a server tool.`
515
+ }, false);
516
+ }
517
+ else if (event.type === "output_update") {
518
+ add("system", `Output ${event.node_id}: ${displayResult("output", undefined, event.value)}`);
519
+ }
520
+ else if (event.type === "error") {
521
+ throw new Error(event.message);
522
+ }
523
+ }
524
+ async function compact(instructions, signal) {
525
+ if (wsUrl) {
526
+ throw new Error("Remote context is managed by the server. Ask the agent to summarize, or use /new for a fresh thread.");
527
+ }
528
+ if (!history.current.length) {
529
+ setStatus("Nothing to compact");
530
+ return;
531
+ }
532
+ setStatus("Compacting");
533
+ const prov = await createProvider(provider);
534
+ let summary = "";
535
+ for await (const item of prov.generateMessagesTraced({
536
+ model,
537
+ signal,
538
+ messages: [
539
+ {
540
+ role: "user",
541
+ content: `Summarize decisions, files, tool results and outstanding work to continue this conversation. ${instructions}\n\n${JSON.stringify(history.current)}`
542
+ }
543
+ ]
544
+ })) {
545
+ if (signal.aborted) {
546
+ return;
547
+ }
548
+ if ("type" in item && item.type === "chunk" && isString(item.content)) {
549
+ summary += item.content;
550
+ }
551
+ }
552
+ if (!summary.trim()) {
553
+ throw new Error("The model returned an empty summary. Context was kept.");
554
+ }
555
+ // The first system message is replaced by applySystemPrompt on each turn.
556
+ history.current = [
557
+ { role: "system", content: "" },
558
+ { role: "system", content: `Retained conversation context:\n${summary}` }
559
+ ];
560
+ providerSession.current = null;
561
+ refreshContextTokens();
562
+ add("system", "Conversation compacted. Context retained.");
563
+ }
564
+ async function command(value, signal) {
565
+ const [name = "", ...words] = value.split(/\s+/);
566
+ const argument = words.join(" ");
567
+ switch (name.toLowerCase()) {
568
+ case "/help":
569
+ add("system", Object.entries(CHAT_COMMANDS)
570
+ .map(([cmd, description]) => `${cmd.padEnd(12)} ${description}`)
571
+ .join("\n") +
572
+ "\n\nEnter send · Alt+Enter or Ctrl+J newline · Tab complete\n↑↓ history · PgUp/PgDn or wheel scroll · Ctrl+G latest\nDrag transcript text to copy · Ctrl+O tool details\nEsc cancel/dismiss · Ctrl+C cancel/clear/quit");
573
+ break;
574
+ case "/clear":
575
+ setClearedCount(transcript.current.length);
576
+ latest();
577
+ break;
578
+ case "/new":
579
+ await persist();
580
+ history.current = [];
581
+ refreshContextTokens();
582
+ providerSession.current = null;
583
+ updateMessages([]);
584
+ thread.current = newSessionId();
585
+ setSessionId(newSessionId());
586
+ sessionAllow.current.clear();
587
+ setClearedCount(0);
588
+ setTodos([]);
589
+ setTasks({});
590
+ agentThreadsRef.current = {};
591
+ setAgentThreads({});
592
+ setSelectedAgentId(null);
593
+ setUsage("");
594
+ latest();
595
+ break;
596
+ case "/compact":
597
+ await compact(argument, signal);
598
+ break;
599
+ case "/model":
600
+ if (!argument) {
601
+ setInput("/model ");
602
+ break;
603
+ }
604
+ {
605
+ const qualified = models.find((item) => `${item.provider}/${item.id}` === argument);
606
+ const matchingIds = models.filter((item) => item.id === argument);
607
+ if (!qualified && matchingIds.length > 1) {
608
+ throw new Error(`Model "${argument}" is available from multiple providers. Choose a provider/model entry.`);
609
+ }
610
+ const inferred = qualified ?? matchingIds[0];
611
+ const separator = argument.indexOf("/");
612
+ const explicitProvider = separator > 0 &&
613
+ configuredProviders.has(argument.slice(0, separator))
614
+ ? argument.slice(0, separator)
615
+ : undefined;
616
+ const nextProvider = inferred?.provider ?? explicitProvider ?? provider;
617
+ const nextModel = inferred
618
+ ? inferred.id
619
+ : explicitProvider
620
+ ? argument.slice(separator + 1)
621
+ : argument;
622
+ setProvider(nextProvider);
623
+ setModel(nextModel);
624
+ providerSession.current = null;
625
+ await saveSettings({ provider: nextProvider, model: nextModel });
626
+ }
627
+ break;
628
+ case "/agent": {
629
+ inspectAgent(argument);
630
+ break;
631
+ }
632
+ case "/mode":
633
+ setMode(parsePermissionMode(argument) ?? "default");
634
+ break;
635
+ case "/sessions": {
636
+ const saved = await store.current.list(workspaceDir, wsUrl);
637
+ setSessions(saved);
638
+ if (!saved.length) {
639
+ add("system", "No saved conversations in this workspace yet.");
640
+ }
641
+ else {
642
+ setInput("/resume ");
643
+ }
644
+ break;
645
+ }
646
+ case "/resume": {
647
+ const saved = await store.current.list(workspaceDir, wsUrl);
648
+ const selected = argument
649
+ ? saved.find((item) => item.id === argument)
650
+ : saved[0];
651
+ if (!selected) {
652
+ throw new Error("Session not found. Use /sessions to choose a saved conversation.");
653
+ }
654
+ await persist();
655
+ restore(selected);
656
+ break;
657
+ }
658
+ case "/export": {
659
+ const path = resolve(workspaceDir, argument || `nodetool-chat-${sessionId}.md`);
660
+ await writeFile(path, exportTranscript(transcript.current), {
661
+ encoding: "utf8",
662
+ flag: "wx",
663
+ mode: 0o600
664
+ });
665
+ add("system", `Exported ${path}`);
666
+ break;
667
+ }
668
+ case "/tools":
669
+ add("system", wsUrl
670
+ ? "Connected server manages the toolbelt. Ask the agent to list its available tools."
671
+ : tools()
672
+ .map((tool) => `${tool.name} — ${tool.description.split("\n")[0]}`)
673
+ .join("\n"));
674
+ break;
675
+ case "/details":
676
+ setDetails((previous) => !previous);
677
+ break;
678
+ case "/exit":
679
+ case "/quit":
680
+ await quit();
681
+ break;
682
+ default:
683
+ throw new Error(`Unknown command: ${name}. Use /help.`);
684
+ }
685
+ }
686
+ function inspectAgent(argument) {
687
+ if (!argument) {
688
+ const threads = Object.values(agentThreadsRef.current);
689
+ if (!threads.length) {
690
+ add("system", "No sub-agent threads in this conversation yet.");
691
+ }
692
+ else {
693
+ setInput("/agent ");
694
+ }
695
+ return;
696
+ }
697
+ if (argument === "main") {
698
+ setSelectedAgentId(null);
699
+ latest();
700
+ return;
701
+ }
702
+ const selectedThread = agentThreadsRef.current[argument];
703
+ if (!selectedThread) {
704
+ throw new Error(`Unknown sub-agent thread: ${argument}. Use /agent.`);
705
+ }
706
+ setSelectedAgentId(selectedThread.id);
707
+ latest();
708
+ }
709
+ async function submit(value) {
710
+ const trimmed = value.trim();
711
+ if (!trimmed) {
712
+ return;
713
+ }
714
+ if (active.current) {
715
+ const [name = "", ...words] = trimmed.split(/\s+/);
716
+ if (name.toLowerCase() === "/agent") {
717
+ setInput("");
718
+ try {
719
+ inspectAgent(words.join(" "));
720
+ }
721
+ catch (error) {
722
+ setStatus(errorText(error));
723
+ }
724
+ }
725
+ return;
726
+ }
727
+ active.current = true;
728
+ const abort = new AbortController();
729
+ controller.current = abort;
730
+ setBusy(true);
731
+ setInput("");
732
+ setStatus("Thinking");
733
+ latest();
734
+ setHistoryIndex(-1);
735
+ setDraft("");
736
+ setInputHistory((previous) => [value, ...previous.filter((item) => item !== value)].slice(0, 100));
737
+ let budget;
738
+ try {
739
+ if (value.startsWith("/")) {
740
+ await command(value.trim(), abort.signal);
741
+ return;
742
+ }
743
+ add("user", value);
744
+ if (wsUrl) {
745
+ const socket = client.current;
746
+ if (!socket || connection !== "Connected") {
747
+ throw new Error("Server is not connected. Restart chat to reconnect.");
748
+ }
749
+ const calls = new Map();
750
+ for await (const event of socket.chat(value, thread.current, model, provider, undefined, { permissionMode: mode, signal: abort.signal })) {
751
+ if (abort.signal.aborted) {
752
+ break;
753
+ }
754
+ if (event.type === "tool_call") {
755
+ calls.set(event.id, event);
756
+ startTool(event.id, event.name, event.args);
757
+ }
758
+ else if (event.type === "tool_result") {
759
+ const call = calls.get(event.id);
760
+ finishTool(event.id, event.name || call?.name || "tool", call?.args, event.content);
761
+ calls.delete(event.id);
762
+ }
763
+ else {
764
+ await remoteEvent(event, socket, abort.signal);
765
+ }
766
+ }
767
+ }
768
+ else {
769
+ const ctx = await createChatContext({ workspaceDir });
770
+ const unsubscribe = ctx.addMessageListener(processEvent);
771
+ try {
772
+ budget = await createCliRunBudget({ costCap, timeout });
773
+ const gate = {
774
+ mode,
775
+ sessionAllow: sessionAllow.current,
776
+ requestApproval: async (request) => {
777
+ const answer = await prompts.current.ask({
778
+ title: `Approve ${request.toolName}`,
779
+ body: `${request.description || request.message}\n\n${JSON.stringify(request.args, null, 2)}`,
780
+ choices: APPROVAL_CHOICES
781
+ }, abort.signal);
782
+ return answer === "allow" || answer === "allow_for_chat"
783
+ ? answer
784
+ : "deny";
785
+ }
786
+ };
787
+ ctx.set(PERMISSION_GATE_CONTEXT_KEY, gate);
788
+ ctx.set(RUN_BUDGET_CONTEXT_KEY, budget);
789
+ const prov = await createProvider(provider);
790
+ const belt = buildCliAgentBelt({
791
+ baseTools: tools(),
792
+ provider: prov,
793
+ model,
794
+ forwardMessage: processEvent,
795
+ gate,
796
+ budget,
797
+ readOnlySearch: enableReadOnlySearch,
798
+ planning: true
799
+ });
800
+ const turn = createCliCodeActTurn({
801
+ tools: belt,
802
+ context: ctx,
803
+ skillCatalog: await loadCliSkillCatalog(ctx.userId),
804
+ signal: abort.signal,
805
+ onToolCall: ({ name }) => setStatus(friendlyToolName(name))
806
+ });
807
+ applySystemPrompt(history.current, turn.systemPrompt);
808
+ refreshContextTokens();
809
+ const historyStart = history.current.length;
810
+ let displayedText = "";
811
+ await processChat({
812
+ userInput: value,
813
+ messages: history.current,
814
+ provider: prov,
815
+ model,
816
+ threadId: thread.current,
817
+ providerSession: providerSession.current,
818
+ context: ctx,
819
+ tools: turn.tools,
820
+ signal: abort.signal,
821
+ turnBudget: budget,
822
+ callbacks: {
823
+ onChunk: (text) => {
824
+ if (!abort.signal.aborted) {
825
+ displayedText += text;
826
+ append(text);
827
+ setStatus("Responding");
828
+ }
829
+ },
830
+ onToolCall: (call) => startTool(call.id, call.name, call.args),
831
+ onToolResult: (call, result) => finishTool(call.id, call.name, call.args, result),
832
+ onProviderSession: (session) => {
833
+ providerSession.current = session;
834
+ }
835
+ }
836
+ });
837
+ const answers = history.current
838
+ .slice(historyStart)
839
+ .filter((message) => message.role === "assistant");
840
+ const finalText = answers
841
+ .map((message) => isString(message.content)
842
+ ? message.content
843
+ : (message.content
844
+ ?.flatMap((part) => part.type === "text" ? [part.text] : [])
845
+ .join("") ?? ""))
846
+ .join("");
847
+ if (finalText.startsWith(displayedText)) {
848
+ append(finalText.slice(displayedText.length));
849
+ }
850
+ else if (finalText && !abort.signal.aborted) {
851
+ flush();
852
+ add("assistant", finalText);
853
+ }
854
+ const attachments = answers.flatMap((message) => attachmentLines(message.content));
855
+ if (attachments.length) {
856
+ flush();
857
+ add("system", attachments.join("\n"));
858
+ }
859
+ }
860
+ finally {
861
+ unsubscribe();
862
+ }
863
+ }
864
+ flush();
865
+ refreshContextTokens();
866
+ if (budget) {
867
+ setUsage(budgetSummaryLine(budget));
868
+ const reason = budgetStopReason(budget);
869
+ if (reason) {
870
+ add("system", `Stopped: ${reason}`);
871
+ }
872
+ }
873
+ }
874
+ catch (error) {
875
+ if (!abort.signal.aborted) {
876
+ add("system", `Error: ${errorText(error)}`);
877
+ }
878
+ }
879
+ finally {
880
+ flush();
881
+ refreshContextTokens();
882
+ if (abort.signal.aborted) {
883
+ add("system", "Stopped. You can continue this conversation.");
884
+ }
885
+ setActiveTools({});
886
+ if (!value.startsWith("/") || value.startsWith("/compact")) {
887
+ try {
888
+ await persist();
889
+ }
890
+ catch (error) {
891
+ add("system", `Session could not be saved: ${errorText(error)}`);
892
+ }
893
+ }
894
+ controller.current = null;
895
+ active.current = false;
896
+ setBusy(false);
897
+ setStatus("Ready");
898
+ }
899
+ }
900
+ const completions = [];
901
+ if (input.startsWith("/") && (!busy || input.startsWith("/agent"))) {
902
+ const space = input.indexOf(" ");
903
+ const name = space < 0 ? input : input.slice(0, space);
904
+ const argument = space < 0 ? "" : input.slice(space + 1).toLowerCase();
905
+ if (space < 0) {
906
+ for (const [value, description] of Object.entries(CHAT_COMMANDS)) {
907
+ if (value.startsWith(name.toLowerCase())) {
908
+ completions.push({ value, description });
909
+ }
910
+ }
911
+ }
912
+ else if (name === "/model") {
913
+ for (const item of models) {
914
+ if (`${item.provider} ${item.id} ${item.name}`
915
+ .toLowerCase()
916
+ .includes(argument)) {
917
+ completions.push({
918
+ value: `/model ${item.provider}/${item.id}`,
919
+ description: item.name
920
+ });
921
+ }
922
+ }
923
+ }
924
+ else if (name === "/agent") {
925
+ if (selectedAgentId && "main".includes(argument)) {
926
+ completions.push({
927
+ value: "/agent main",
928
+ description: "Return to the main conversation"
929
+ });
930
+ }
931
+ for (const thread of Object.values(agentThreads)) {
932
+ if (`${thread.id} ${thread.title}`.toLowerCase().includes(argument)) {
933
+ completions.push({
934
+ value: `/agent ${thread.id}`,
935
+ description: `${thread.status} · ${thread.title}`
936
+ });
937
+ }
938
+ }
939
+ }
940
+ else if (name === "/mode") {
941
+ for (const modeName of ["default", "auto", "plan"]) {
942
+ if (modeName.startsWith(argument)) {
943
+ completions.push({
944
+ value: `/mode ${modeName}`,
945
+ description: "Permission mode"
946
+ });
947
+ }
948
+ }
949
+ }
950
+ else if (name === "/resume") {
951
+ for (const session of sessions) {
952
+ if (`${session.id} ${session.title}`.toLowerCase().includes(argument)) {
953
+ completions.push({
954
+ value: `/resume ${session.id}`,
955
+ description: session.title
956
+ });
957
+ }
958
+ }
959
+ }
960
+ }
961
+ const selected = Math.min(completionIndex, Math.max(0, completions.length - 1));
962
+ function changeInput(value) {
963
+ setInput(value);
964
+ setCompletionIndex(0);
965
+ }
966
+ function complete(direction) {
967
+ if (direction === "up" || direction === "down") {
968
+ setCompletionIndex((selected + (direction === "up" ? -1 : 1) + completions.length) %
969
+ completions.length);
970
+ return;
971
+ }
972
+ const item = completions[selected];
973
+ if (!item) {
974
+ return;
975
+ }
976
+ if (direction === "accept") {
977
+ changeInput(item.value + (item.value.includes(" ") ? "" : " "));
978
+ return;
979
+ }
980
+ if (item.value.includes(" ")) {
981
+ void submit(item.value);
982
+ return;
983
+ }
984
+ if (["/model", "/agent", "/mode", "/resume", "/export"].includes(item.value)) {
985
+ changeInput(`${item.value} `);
986
+ }
987
+ else {
988
+ void submit(item.value);
989
+ }
990
+ }
991
+ function recall(direction) {
992
+ if (busy || !inputHistory.length) {
993
+ return;
994
+ }
995
+ if (historyIndex < 0) {
996
+ setDraft(input);
997
+ }
998
+ const next = direction === "up"
999
+ ? Math.min(historyIndex + 1, inputHistory.length - 1)
1000
+ : Math.max(-1, historyIndex - 1);
1001
+ setHistoryIndex(next);
1002
+ changeInput(next < 0 ? draft : (inputHistory[next] ?? ""));
1003
+ }
1004
+ useInput((keyInput, key) => {
1005
+ // Ink re-registers this handler in an effect after each render, so a key
1006
+ // pressed as a prompt opens can reach the handler from before it. Read the
1007
+ // live prompt rather than the rendered state.
1008
+ const pending = prompts.current.current;
1009
+ if (pending) {
1010
+ const choice = pending.choices.find((item) => item.key === keyInput.toLowerCase());
1011
+ if (choice) {
1012
+ prompts.current.answer(choice.value);
1013
+ }
1014
+ if (key.escape) {
1015
+ prompts.current.answer("cancel");
1016
+ }
1017
+ if (!(key.ctrl && keyInput === "c")) {
1018
+ return;
1019
+ }
1020
+ }
1021
+ if (key.ctrl && keyInput === "o") {
1022
+ setDetails((previous) => !previous);
1023
+ return;
1024
+ }
1025
+ if (key.escape || (key.ctrl && keyInput === "c")) {
1026
+ if (active.current) {
1027
+ controller.current?.abort();
1028
+ setStatus("Stopping…");
1029
+ }
1030
+ else if (input) {
1031
+ changeInput("");
1032
+ }
1033
+ else if (key.ctrl) {
1034
+ void quit();
1035
+ }
1036
+ }
1037
+ });
1038
+ const sidebarWidth = columns >= 112 ? 30 : 0;
1039
+ const width = Math.max(1, columns - 4 - sidebarWidth);
1040
+ const editorRows = Math.min(5, Math.max(1, terminalLines(input || " ", Math.max(1, columns - 6)).length));
1041
+ const menuRows = Math.min(5, completions.length, Math.max(0, rows - 14));
1042
+ const bodyHeight = Math.max(1, rows - editorRows - menuRows - 8);
1043
+ const menuStart = Math.max(0, selected - menuRows + 1);
1044
+ const work = [
1045
+ ...todos.map((todo) => `${todo.status === "completed" ? "✓" : todo.status === "in_progress" ? "›" : "○"} ${todo.content}`),
1046
+ ...Object.values(tasks),
1047
+ ...Object.values(activeTools).map((tool) => `› ${friendlyToolName(tool.name)}`)
1048
+ ];
1049
+ const selectedAgent = selectedAgentId
1050
+ ? agentThreads[selectedAgentId]
1051
+ : undefined;
1052
+ return (_jsxs(Box, { flexDirection: "column", width: columns, height: rows, overflow: "hidden", paddingX: 1, children: [_jsxs(Box, { height: 1, justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: "cyan", children: "NodeTool" }), _jsxs(Text, { dimColor: true, wrap: "truncate", children: [terminalText(provider), " / ", terminalText(model)] })] }), _jsxs(Text, { dimColor: true, wrap: "truncate", children: [terminalText(workspaceDir), " \u00B7 ", connection, " \u00B7 ", mode] }), _jsx(Text, { dimColor: true, children: "─".repeat(Math.max(1, columns - 2)) }), _jsxs(Box, { height: bodyHeight, flexShrink: 0, children: [prompt ? (_jsxs(Box, { width: width, height: bodyHeight, flexDirection: "column", overflow: "hidden", children: [_jsx(Text, { bold: true, color: "yellow", children: terminalText(prompt.title) }), _jsx(Transcript, { mouseEvents: mouseEvents, screenY: 5, messages: [
1053
+ { id: "prompt", role: "system", content: prompt.body }
1054
+ ], live: "", width: width, height: Math.max(1, bodyHeight - 3), details: true, resetKey: prompt.title, startAtTop: true }), _jsx(Text, { color: "cyan", wrap: "truncate", children: prompt.choices
1055
+ .map((choice) => `[${choice.key}] ${choice.label}`)
1056
+ .join(" ") }), _jsx(Text, { dimColor: true, children: "Esc dismiss \u00B7 Ctrl+C stop" })] })) : selectedAgent ? (_jsxs(Box, { width: width, height: bodyHeight, flexDirection: "column", overflow: "hidden", children: [_jsxs(Text, { bold: true, color: "cyan", wrap: "truncate", children: [terminalText(selectedAgent.title), " \u00B7 ", selectedAgent.status] }), _jsx(Transcript, { mouseEvents: mouseEvents, screenY: 5, messages: selectedAgent.messages, live: "", width: width, height: Math.max(1, bodyHeight - 2), details: details, resetKey: `${selectedAgent.id}-${scrollKey}` }), _jsx(Text, { dimColor: true, children: "/agent main returns to the conversation" })] })) : (_jsx(Transcript, { mouseEvents: mouseEvents, messages: messages.slice(clearedCount), live: live, width: width, height: bodyHeight, details: details, resetKey: `${sessionId}-${scrollKey}` })), sidebarWidth > 0 && (_jsxs(Box, { width: sidebarWidth, paddingLeft: 2, flexDirection: "column", overflow: "hidden", children: [_jsx(Text, { bold: true, children: "Session" }), _jsx(Text, { dimColor: true, children: sessionId.slice(0, 12) }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Context" }), _jsx(Text, { dimColor: true, children: wsUrl
1057
+ ? "Managed by server"
1058
+ : contextTokens === null
1059
+ ? "Not measured yet"
1060
+ : `~${contextTokens.toLocaleString()} tokens` }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Work" }), _jsx(Text, { dimColor: true, children: terminalLines(terminalText(work.join("\n") || "No active tasks"), sidebarWidth - 2)
1061
+ .slice(0, Math.max(1, bodyHeight - 9))
1062
+ .join("\n") }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Agents" }), _jsx(Text, { dimColor: true, children: terminalLines(terminalText(Object.values(agentThreads)
1063
+ .map((thread) => `${thread.status === "running" ? "›" : thread.status === "completed" ? "✓" : "×"} ${thread.title}`)
1064
+ .join("\n") || "No sub-agents"), sidebarWidth - 2)
1065
+ .slice(0, 5)
1066
+ .join("\n") })] }))] }), _jsx(Box, { height: 1, children: _jsxs(Text, { color: prompt ? "yellow" : busy ? "cyan" : "gray", wrap: "truncate", children: [busy && _jsx(Spinner, { type: "dots" }), " ", terminalText(status), busy ? ` · ${elapsed}s · Esc stop` : usage ? ` · ${usage}` : ""] }) }), menuRows > 0 && (_jsx(Box, { height: menuRows, flexDirection: "column", children: completions.slice(menuStart, menuStart + menuRows).map((item, i) => (_jsxs(Text, { color: menuStart + i === selected ? "cyan" : "gray", wrap: "truncate", children: [menuStart + i === selected ? "› " : " ", terminalText(item.value), " ", terminalText(item.description)] }, item.value))) })), _jsx(Text, { dimColor: true, children: "─".repeat(Math.max(1, columns - 2)) }), _jsxs(Box, { height: editorRows, children: [_jsx(Text, { color: "cyan", children: "\u203A " }), _jsx(ReadlineInput, { value: input, onChange: changeInput, onSubmit: submit, onHistory: recall, onComplete: completions.length ? complete : undefined, width: Math.max(1, columns - 6), maxRows: editorRows, focus: !prompt, placeholder: busy ? "Draft your next message…" : "Ask NodeTool…" })] }), _jsx(Text, { dimColor: true, children: "─".repeat(Math.max(1, columns - 2)) }), _jsx(Text, { dimColor: true, wrap: "truncate", children: columns < 70
1067
+ ? "Enter send · Ctrl+J newline · /help"
1068
+ : "Enter send · Alt+Enter newline · Wheel scroll · Drag to copy · /help" })] }));
1069
+ }
1070
+ //# sourceMappingURL=app.js.map