@sheepbun/yips 0.1.1 → 0.1.47

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 (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +52 -0
  3. package/bin/yips.js +15 -0
  4. package/package.json +21 -128
  5. package/postinstall.js +52 -0
  6. package/dist/agent/commands/command-catalog.js +0 -243
  7. package/dist/agent/commands/commands.js +0 -418
  8. package/dist/agent/conductor.js +0 -118
  9. package/dist/agent/context/code-context.js +0 -68
  10. package/dist/agent/context/memory-store.js +0 -159
  11. package/dist/agent/context/session-store.js +0 -211
  12. package/dist/agent/protocol/tool-protocol.js +0 -160
  13. package/dist/agent/skills/skills.js +0 -327
  14. package/dist/agent/tools/tool-executor.js +0 -415
  15. package/dist/agent/tools/tool-safety.js +0 -52
  16. package/dist/app/index.js +0 -35
  17. package/dist/app/repl.js +0 -105
  18. package/dist/app/update-check.js +0 -132
  19. package/dist/app/version.js +0 -51
  20. package/dist/code-context.js +0 -68
  21. package/dist/colors.js +0 -204
  22. package/dist/command-catalog.js +0 -242
  23. package/dist/commands.js +0 -350
  24. package/dist/conductor.js +0 -94
  25. package/dist/config/config.js +0 -335
  26. package/dist/config/hooks.js +0 -187
  27. package/dist/config.js +0 -335
  28. package/dist/downloader-state.js +0 -302
  29. package/dist/downloader-ui.js +0 -289
  30. package/dist/gateway/adapters/discord.js +0 -108
  31. package/dist/gateway/adapters/formatting.js +0 -96
  32. package/dist/gateway/adapters/telegram.js +0 -106
  33. package/dist/gateway/adapters/types.js +0 -2
  34. package/dist/gateway/adapters/whatsapp.js +0 -124
  35. package/dist/gateway/auth-policy.js +0 -66
  36. package/dist/gateway/core.js +0 -87
  37. package/dist/gateway/headless-conductor.js +0 -328
  38. package/dist/gateway/message-router.js +0 -23
  39. package/dist/gateway/rate-limiter.js +0 -48
  40. package/dist/gateway/runtime/backend-policy.js +0 -18
  41. package/dist/gateway/runtime/discord-bot.js +0 -104
  42. package/dist/gateway/runtime/discord-main.js +0 -69
  43. package/dist/gateway/session-manager.js +0 -77
  44. package/dist/gateway/types.js +0 -2
  45. package/dist/hardware.js +0 -92
  46. package/dist/hooks.js +0 -187
  47. package/dist/index.js +0 -34
  48. package/dist/input-engine.js +0 -250
  49. package/dist/llama-client.js +0 -227
  50. package/dist/llama-server.js +0 -620
  51. package/dist/llm/llama-client.js +0 -227
  52. package/dist/llm/llama-server.js +0 -620
  53. package/dist/llm/token-counter.js +0 -47
  54. package/dist/memory-store.js +0 -159
  55. package/dist/messages.js +0 -59
  56. package/dist/model-downloader.js +0 -382
  57. package/dist/model-manager-state.js +0 -118
  58. package/dist/model-manager-ui.js +0 -194
  59. package/dist/model-manager.js +0 -190
  60. package/dist/models/hardware.js +0 -92
  61. package/dist/models/model-downloader.js +0 -382
  62. package/dist/models/model-manager.js +0 -190
  63. package/dist/prompt-box.js +0 -78
  64. package/dist/prompt-composer.js +0 -498
  65. package/dist/repl.js +0 -105
  66. package/dist/session-store.js +0 -211
  67. package/dist/spinner.js +0 -76
  68. package/dist/title-box.js +0 -388
  69. package/dist/token-counter.js +0 -47
  70. package/dist/tool-executor.js +0 -415
  71. package/dist/tool-protocol.js +0 -121
  72. package/dist/tool-safety.js +0 -52
  73. package/dist/tui/app.js +0 -2553
  74. package/dist/tui/startup.js +0 -56
  75. package/dist/tui-input-routing.js +0 -53
  76. package/dist/tui.js +0 -51
  77. package/dist/types/app-types.js +0 -2
  78. package/dist/types.js +0 -2
  79. package/dist/ui/colors.js +0 -204
  80. package/dist/ui/downloader/downloader-state.js +0 -302
  81. package/dist/ui/downloader/downloader-ui.js +0 -289
  82. package/dist/ui/input/input-engine.js +0 -250
  83. package/dist/ui/input/tui-input-routing.js +0 -53
  84. package/dist/ui/input/vt-session.js +0 -168
  85. package/dist/ui/messages.js +0 -59
  86. package/dist/ui/model-manager/model-manager-state.js +0 -118
  87. package/dist/ui/model-manager/model-manager-ui.js +0 -194
  88. package/dist/ui/prompt/prompt-box.js +0 -78
  89. package/dist/ui/prompt/prompt-composer.js +0 -498
  90. package/dist/ui/spinner.js +0 -76
  91. package/dist/ui/title-box.js +0 -388
  92. package/dist/ui/tui/app.js +0 -6
  93. package/dist/ui/tui/autocomplete.js +0 -85
  94. package/dist/ui/tui/constants.js +0 -18
  95. package/dist/ui/tui/history.js +0 -29
  96. package/dist/ui/tui/layout.js +0 -341
  97. package/dist/ui/tui/runtime-core.js +0 -2584
  98. package/dist/ui/tui/runtime-utils.js +0 -53
  99. package/dist/ui/tui/start-tui.js +0 -54
  100. package/dist/ui/tui/startup.js +0 -56
  101. package/dist/version.js +0 -51
  102. package/dist/vt-session.js +0 -168
  103. package/install.sh +0 -457
package/dist/tui/app.js DELETED
@@ -1,2553 +0,0 @@
1
- "use strict";
2
- /** Main TUI orchestrator using Ink. */
3
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
- if (k2 === undefined) k2 = k;
5
- var desc = Object.getOwnPropertyDescriptor(m, k);
6
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
- desc = { enumerable: true, get: function() { return m[k]; } };
8
- }
9
- Object.defineProperty(o, k2, desc);
10
- }) : (function(o, m, k, k2) {
11
- if (k2 === undefined) k2 = k;
12
- o[k2] = m[k];
13
- }));
14
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
- Object.defineProperty(o, "default", { enumerable: true, value: v });
16
- }) : function(o, v) {
17
- o["default"] = v;
18
- });
19
- var __importStar = (this && this.__importStar) || (function () {
20
- var ownKeys = function(o) {
21
- ownKeys = Object.getOwnPropertyNames || function (o) {
22
- var ar = [];
23
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
- return ar;
25
- };
26
- return ownKeys(o);
27
- };
28
- return function (mod) {
29
- if (mod && mod.__esModule) return mod;
30
- var result = {};
31
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
- __setModuleDefault(result, mod);
33
- return result;
34
- };
35
- })();
36
- Object.defineProperty(exports, "__esModule", { value: true });
37
- exports.resolveModelLoadTarget = resolveModelLoadTarget;
38
- exports.formatModelLoadingLabel = formatModelLoadingLabel;
39
- exports.runOnceGuarded = runOnceGuarded;
40
- exports.computeTokensPerSecond = computeTokensPerSecond;
41
- exports.formatTokensPerSecond = formatTokensPerSecond;
42
- exports.formatTitleCwd = formatTitleCwd;
43
- exports.buildPromptStatusText = buildPromptStatusText;
44
- exports.composeOutputLines = composeOutputLines;
45
- exports.renderHistoryLines = renderHistoryLines;
46
- exports.composeChatRequestMessages = composeChatRequestMessages;
47
- exports.buildModelAutocompleteCandidates = buildModelAutocompleteCandidates;
48
- exports.shouldConsumeSubmitForAutocomplete = shouldConsumeSubmitForAutocomplete;
49
- exports.computeVisibleLayoutSlices = computeVisibleLayoutSlices;
50
- exports.computeTitleVisibleScrollCap = computeTitleVisibleScrollCap;
51
- exports.buildAutocompleteOverlayLines = buildAutocompleteOverlayLines;
52
- exports.buildPromptRenderLines = buildPromptRenderLines;
53
- exports.createInkApp = createInkApp;
54
- const react_1 = __importStar(require("react"));
55
- const node_fs_1 = require("node:fs");
56
- const node_path_1 = require("node:path");
57
- const commands_1 = require("../commands");
58
- const config_1 = require("../config");
59
- const colors_1 = require("../colors");
60
- const llama_client_1 = require("../llama-client");
61
- const llama_server_1 = require("../llama-server");
62
- const messages_1 = require("../messages");
63
- const spinner_1 = require("../spinner");
64
- const prompt_box_1 = require("../prompt-box");
65
- const prompt_composer_1 = require("../prompt-composer");
66
- const input_engine_1 = require("../input-engine");
67
- const downloader_ui_1 = require("../downloader-ui");
68
- const downloader_state_1 = require("../downloader-state");
69
- const hardware_1 = require("../hardware");
70
- const model_manager_1 = require("../model-manager");
71
- const model_manager_ui_1 = require("../model-manager-ui");
72
- const model_manager_state_1 = require("../model-manager-state");
73
- const model_downloader_1 = require("../model-downloader");
74
- const title_box_1 = require("../title-box");
75
- const token_counter_1 = require("../token-counter");
76
- const conductor_1 = require("../conductor");
77
- const hooks_1 = require("../hooks");
78
- const session_store_1 = require("../session-store");
79
- const tool_safety_1 = require("../tool-safety");
80
- const tool_executor_1 = require("../tool-executor");
81
- const vt_session_1 = require("../vt-session");
82
- const code_context_1 = require("../code-context");
83
- const tui_input_routing_1 = require("../tui-input-routing");
84
- const PROMPT_PREFIX = ">>> ";
85
- const CURSOR_MARKER = "▌";
86
- const KEY_DEBUG_ENABLED = process.env["YIPS_DEBUG_KEYS"] === "1";
87
- const ANSI_REVERSE_ON = "\u001b[7m";
88
- const ANSI_RESET_ALL = "\u001b[0m";
89
- const DOWNLOADER_MIN_SEARCH_CHARS = 3;
90
- const DOWNLOADER_SEARCH_DEBOUNCE_MS = 400;
91
- const DOWNLOADER_PROGRESS_RENDER_INTERVAL_MS = 200;
92
- const BUSY_SPINNER_RENDER_INTERVAL_MS = 16;
93
- const MOUSE_SCROLL_LINE_STEP = 3;
94
- const ENABLE_MOUSE_REPORTING = "\u001b[?1000h\u001b[?1006h";
95
- const DISABLE_MOUSE_REPORTING = "\u001b[?1000l\u001b[?1006l";
96
- const ANSI_SGR_PATTERN = new RegExp(String.raw `\u001b\[[0-9;]*m`, "g");
97
- function formatBackendName(backend) {
98
- return backend === "llamacpp" ? "llama.cpp" : backend;
99
- }
100
- function resolveLoadedModel(model) {
101
- const trimmed = model.trim();
102
- if (trimmed.length === 0) {
103
- return null;
104
- }
105
- if (trimmed.toLowerCase() === "default") {
106
- return null;
107
- }
108
- return trimmed;
109
- }
110
- function resolveModelLoadTarget(config) {
111
- return config.llamaGpuLayers > 0 ? "GPU" : "CPU";
112
- }
113
- function formatModelLoadingLabel(config, nicknames) {
114
- const loadedModel = resolveLoadedModel(config.model);
115
- const modelLabel = loadedModel ? (0, model_manager_1.getFriendlyModelName)(loadedModel, nicknames) : "model";
116
- const target = resolveModelLoadTarget(config);
117
- return `Loading ${modelLabel} into ${target}...`;
118
- }
119
- function charLength(text) {
120
- return Array.from(text).length;
121
- }
122
- function clipPromptStatusText(statusText, maxWidth) {
123
- if (maxWidth <= 0)
124
- return "";
125
- const trimmed = statusText.trim();
126
- const normalized = trimmed.length > 0 ? ` ${trimmed} ` : " ";
127
- const chars = Array.from(normalized);
128
- if (chars.length <= maxWidth)
129
- return normalized;
130
- return chars.slice(chars.length - maxWidth).join("");
131
- }
132
- function formatBytes(bytes) {
133
- if (!Number.isFinite(bytes) || bytes <= 0) {
134
- return "0 B";
135
- }
136
- const units = ["B", "KB", "MB", "GB", "TB"];
137
- let value = bytes;
138
- let unitIndex = 0;
139
- while (value >= 1024 && unitIndex < units.length - 1) {
140
- value /= 1024;
141
- unitIndex += 1;
142
- }
143
- const precision = unitIndex >= 2 ? 1 : 0;
144
- return `${value.toFixed(precision)} ${units[unitIndex]}`;
145
- }
146
- async function runOnceGuarded(guard, operation) {
147
- if (guard.current) {
148
- return false;
149
- }
150
- guard.current = true;
151
- await operation();
152
- return true;
153
- }
154
- function formatEta(totalSeconds) {
155
- const safeSeconds = Math.max(0, Math.floor(totalSeconds));
156
- const minutes = Math.floor(safeSeconds / 60);
157
- const seconds = safeSeconds % 60;
158
- return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
159
- }
160
- function formatDownloadStatus(options) {
161
- const elapsedSeconds = Math.max(0.001, (Date.now() - options.startedAtMs) / 1000);
162
- const bytesPerSecond = options.bytesDownloaded / elapsedSeconds;
163
- const speedText = `${formatBytes(bytesPerSecond)}/s`;
164
- if (options.totalBytes === null || options.totalBytes <= 0) {
165
- return `${formatBytes(options.bytesDownloaded)} downloaded | ${speedText}`;
166
- }
167
- const remainingBytes = Math.max(0, options.totalBytes - options.bytesDownloaded);
168
- const etaSeconds = bytesPerSecond > 0 ? remainingBytes / bytesPerSecond : 0;
169
- return `${formatBytes(options.bytesDownloaded)} / ${formatBytes(options.totalBytes)} | ${speedText} | ETA ${formatEta(etaSeconds)}`;
170
- }
171
- function computeTokensPerSecond(tokens, durationMs) {
172
- if (!Number.isFinite(tokens) || !Number.isFinite(durationMs)) {
173
- return null;
174
- }
175
- if (tokens <= 0 || durationMs <= 0) {
176
- return null;
177
- }
178
- return tokens / (durationMs / 1000);
179
- }
180
- function formatTokensPerSecond(tokensPerSecond) {
181
- const safe = Number.isFinite(tokensPerSecond) && tokensPerSecond > 0 ? tokensPerSecond : 0;
182
- return `${safe.toFixed(1)} tk/s`;
183
- }
184
- function toDebugText(input) {
185
- return Array.from(input)
186
- .map((char) => {
187
- const codePoint = char.codePointAt(0);
188
- if (codePoint === undefined)
189
- return "";
190
- if (codePoint === 0x1b)
191
- return "<ESC>";
192
- if (codePoint === 0x0d)
193
- return "<CR>";
194
- if (codePoint === 0x0a)
195
- return "<LF>";
196
- if (codePoint === 0x08)
197
- return "<BS>";
198
- if (codePoint === 0x7f)
199
- return "<DEL>";
200
- if (codePoint < 0x20 || codePoint === 0x7f) {
201
- return `<0x${codePoint.toString(16).padStart(2, "0")}>`;
202
- }
203
- return char;
204
- })
205
- .join("");
206
- }
207
- function toDebugBytes(input) {
208
- return Array.from(Buffer.from(input, "latin1"))
209
- .map((byte) => byte.toString(16).padStart(2, "0"))
210
- .join(" ");
211
- }
212
- function formatTitleCwd(cwd) {
213
- const trimmed = cwd.trim();
214
- return trimmed.length > 0 ? trimmed : "/";
215
- }
216
- function buildTitleBoxOptions(state, version, width) {
217
- const loadedModel = resolveLoadedModel(state.config.model);
218
- const modelLabel = loadedModel ? (0, model_manager_1.getFriendlyModelName)(loadedModel, state.config.nicknames) : "";
219
- let tokenUsage = "";
220
- if (loadedModel) {
221
- const modelSizeBytes = resolveLoadedModelSizeBytes(state.config, loadedModel);
222
- const autoMax = (0, token_counter_1.computeAutoMaxTokens)({
223
- ramGb: (0, hardware_1.getSystemSpecs)().ramGb,
224
- modelSizeBytes
225
- });
226
- const maxTokens = (0, token_counter_1.resolveEffectiveMaxTokens)(state.config.tokensMode, state.config.tokensManualMax, autoMax);
227
- tokenUsage = (0, token_counter_1.formatTitleTokenUsage)(state.usedTokensExact ?? 0, maxTokens);
228
- }
229
- return {
230
- width,
231
- version,
232
- username: state.username,
233
- backend: formatBackendName(state.config.backend),
234
- model: modelLabel,
235
- tokenUsage,
236
- cwd: formatTitleCwd(process.cwd()),
237
- sessionName: state.sessionName,
238
- recentActivity: state.recentActivity,
239
- sessionSelection: state.uiMode === "sessions"
240
- ? {
241
- active: true,
242
- selectedIndex: state.sessionSelectionIndex
243
- }
244
- : undefined
245
- };
246
- }
247
- function resolveLoadedModelSizeBytes(config, loadedModel) {
248
- try {
249
- const modelsDir = (0, node_path_1.resolve)(config.llamaModelsDir);
250
- const modelPath = (0, node_path_1.resolve)((0, node_path_1.join)(modelsDir, loadedModel));
251
- const stats = (0, node_fs_1.statSync)(modelPath);
252
- return stats.isFile() ? stats.size : 0;
253
- }
254
- catch {
255
- return 0;
256
- }
257
- }
258
- function buildPromptStatusText(state) {
259
- if (state.uiMode === "confirm") {
260
- return "confirmation · required";
261
- }
262
- if (state.uiMode === "vt") {
263
- return "virtual-terminal · active";
264
- }
265
- if (state.uiMode === "sessions") {
266
- return "sessions · browse";
267
- }
268
- if (state.uiMode === "model-manager") {
269
- return "model-manager · search";
270
- }
271
- if (state.uiMode === "downloader") {
272
- return "model-downloader · search";
273
- }
274
- const provider = formatBackendName(state.config.backend);
275
- const loadedModel = resolveLoadedModel(state.config.model);
276
- const parts = [provider];
277
- if (loadedModel) {
278
- parts.push((0, model_manager_1.getFriendlyModelName)(loadedModel, state.config.nicknames));
279
- if (typeof state.latestOutputTokensPerSecond === "number" &&
280
- state.latestOutputTokensPerSecond > 0) {
281
- parts.push(formatTokensPerSecond(state.latestOutputTokensPerSecond));
282
- }
283
- }
284
- const status = parts.join(" · ");
285
- if (state.outputScrollOffset > 0) {
286
- return `${status} · scroll +${state.outputScrollOffset}`;
287
- }
288
- return status;
289
- }
290
- function composeOutputLines(options) {
291
- const lines = [...options.outputLines, ...options.autocompleteOverlay];
292
- if (options.busyLine && options.busyLine.length > 0) {
293
- lines.push(options.busyLine);
294
- }
295
- return lines;
296
- }
297
- function shiftOutputScrollOffsetWithCap(state, delta, maxOffsetCap) {
298
- const next = state.outputScrollOffset + delta;
299
- const clampedCap = Math.max(0, maxOffsetCap);
300
- state.outputScrollOffset = Math.max(0, Math.min(clampedCap, next));
301
- }
302
- function appendOutput(state, text) {
303
- const lines = text.split("\n");
304
- for (const line of lines) {
305
- state.outputLines.push(line);
306
- }
307
- state.outputScrollOffset = 0;
308
- }
309
- function stripAnsi(text) {
310
- return text.replace(ANSI_SGR_PATTERN, "");
311
- }
312
- function isVisuallyEmptyLine(line) {
313
- return stripAnsi(line).trim().length === 0;
314
- }
315
- const TITLE_OUTPUT_GAP_ROWS = 1;
316
- function visibleCharLength(line) {
317
- return Array.from(stripAnsi(line)).length;
318
- }
319
- function inferRenderWidth(titleLines, promptLines) {
320
- const candidates = [...titleLines, ...promptLines]
321
- .map((line) => visibleCharLength(line))
322
- .filter((length) => length > 0);
323
- if (candidates.length === 0) {
324
- return 80;
325
- }
326
- return Math.max(1, ...candidates);
327
- }
328
- function lineDisplayRows(line, width) {
329
- const safeWidth = Math.max(1, width);
330
- const length = visibleCharLength(line);
331
- if (length <= 0) {
332
- return 1;
333
- }
334
- return Math.max(1, Math.ceil(length / safeWidth));
335
- }
336
- function countDisplayRows(lines, width) {
337
- return lines.reduce((total, line) => total + lineDisplayRows(line, width), 0);
338
- }
339
- function trimEndByDisplayRows(line, rowsToTrim, width) {
340
- if (rowsToTrim <= 0) {
341
- return line;
342
- }
343
- const safeWidth = Math.max(1, width);
344
- const plainChars = Array.from(stripAnsi(line));
345
- if (plainChars.length === 0) {
346
- return "";
347
- }
348
- const nextLength = Math.max(0, plainChars.length - rowsToTrim * safeWidth);
349
- return plainChars.slice(0, nextLength).join("");
350
- }
351
- function dropLeadingByDisplayRows(lines, rowsToDrop, width) {
352
- if (rowsToDrop <= 0 || lines.length === 0) {
353
- return [...lines];
354
- }
355
- let index = 0;
356
- let remaining = rowsToDrop;
357
- while (index < lines.length && remaining > 0) {
358
- remaining -= lineDisplayRows(lines[index] ?? "", width);
359
- index += 1;
360
- }
361
- return lines.slice(index);
362
- }
363
- function dropTrailingByDisplayRows(lines, rowsToDrop, width) {
364
- if (rowsToDrop <= 0 || lines.length === 0) {
365
- return [...lines];
366
- }
367
- const next = [...lines];
368
- let remaining = rowsToDrop;
369
- while (next.length > 0 && remaining > 0) {
370
- const lastIndex = next.length - 1;
371
- const lastLine = next[lastIndex] ?? "";
372
- const lastRows = lineDisplayRows(lastLine, width);
373
- if (remaining >= lastRows) {
374
- remaining -= lastRows;
375
- next.pop();
376
- continue;
377
- }
378
- next[lastIndex] = trimEndByDisplayRows(lastLine, remaining, width);
379
- remaining = 0;
380
- }
381
- return next;
382
- }
383
- function computeMinVisibleContentRows(outputLines, width) {
384
- const firstContentIndex = outputLines.findIndex((line) => !isVisuallyEmptyLine(line));
385
- if (firstContentIndex === -1) {
386
- return 0;
387
- }
388
- // Keep the very first visible content row anchored when fully scrolled up.
389
- // For wrapped first lines, this still allows scrolling through later wrapped rows.
390
- const rowsBeforeFirstContent = countDisplayRows(outputLines.slice(0, firstContentIndex), width);
391
- return rowsBeforeFirstContent + 1;
392
- }
393
- function computeMaxOutputScrollOffsetRows(outputLines, width) {
394
- const totalRows = countDisplayRows(outputLines, width);
395
- const minVisibleRows = computeMinVisibleContentRows(outputLines, width);
396
- return Math.max(0, totalRows - minVisibleRows);
397
- }
398
- function computeUsefulOutputScrollCapRows(options) {
399
- const structuralMax = computeMaxOutputScrollOffsetRows(options.outputLines, options.width);
400
- const safeRows = Math.max(1, options.rows);
401
- const promptCount = Math.min(options.promptLines.length, safeRows);
402
- const upperRowCount = Math.max(0, safeRows - promptCount);
403
- const topVisibleTitleCount = Math.min(options.titleLines.length, upperRowCount);
404
- const topGapRows = topVisibleTitleCount > 0 ? TITLE_OUTPUT_GAP_ROWS : 0;
405
- const topContentRows = Math.max(0, upperRowCount - topVisibleTitleCount - topGapRows);
406
- const extraRowsBeyondAnchor = Math.max(0, topContentRows - 1);
407
- return Math.max(0, structuralMax - extraRowsBeyondAnchor);
408
- }
409
- function pruneModelSwitchStatusArtifacts(state) {
410
- const keep = [];
411
- for (const line of state.outputLines) {
412
- const plain = stripAnsi(line).trim();
413
- const isArtifact = plain.startsWith("Model set to: ") ||
414
- plain === "Model preload complete." ||
415
- /^Loading .+ into (GPU|CPU)\.\.\.$/.test(plain);
416
- if (!isArtifact) {
417
- keep.push(line);
418
- }
419
- }
420
- while (keep.length > 0 && keep[keep.length - 1]?.trim().length === 0) {
421
- keep.pop();
422
- }
423
- state.outputLines = keep;
424
- state.outputScrollOffset = 0;
425
- }
426
- function replaceOutputBlock(state, start, count, text) {
427
- const lines = text.split("\n");
428
- state.outputLines.splice(start, count, ...lines);
429
- state.outputScrollOffset = 0;
430
- return lines.length;
431
- }
432
- function resetSession(state) {
433
- state.outputLines = [];
434
- state.outputScrollOffset = 0;
435
- state.messageCount = 0;
436
- state.history = [];
437
- state.sessionFilePath = null;
438
- state.sessionCreated = false;
439
- state.sessionName = "";
440
- state.usedTokensExact = null;
441
- state.latestOutputTokensPerSecond = null;
442
- state.pendingConfirmation = null;
443
- }
444
- function renderHistoryLines(history) {
445
- const lines = [];
446
- let userCount = 0;
447
- for (const entry of history) {
448
- if (entry.role === "user") {
449
- userCount += 1;
450
- lines.push(...(0, messages_1.formatUserMessage)(entry.content).split("\n"));
451
- continue;
452
- }
453
- if (entry.role === "assistant") {
454
- lines.push(...(0, messages_1.formatAssistantMessage)(entry.content).split("\n"));
455
- lines.push("");
456
- continue;
457
- }
458
- lines.push(...(0, messages_1.formatDimMessage)(`[system] ${entry.content}`).split("\n"));
459
- }
460
- return { lines, userCount };
461
- }
462
- function replayOutputFromHistory(state) {
463
- const rendered = renderHistoryLines(state.history);
464
- state.outputLines = rendered.lines;
465
- state.outputScrollOffset = 0;
466
- state.messageCount = rendered.userCount;
467
- }
468
- function createRuntimeState(options) {
469
- const modelOverride = options.model?.trim();
470
- const runtimeConfig = {
471
- ...options.config,
472
- model: modelOverride && modelOverride.length > 0 ? modelOverride : options.config.model
473
- };
474
- return {
475
- outputLines: [],
476
- outputScrollOffset: 0,
477
- running: true,
478
- config: runtimeConfig,
479
- messageCount: 0,
480
- username: options.username ?? process.env["USER"] ?? "user",
481
- sessionName: options.sessionName ?? "",
482
- inputHistory: [],
483
- history: [],
484
- busy: false,
485
- busyLabel: "",
486
- uiMode: "chat",
487
- downloader: null,
488
- modelManager: null,
489
- sessionFilePath: null,
490
- sessionCreated: false,
491
- recentActivity: [],
492
- sessionList: [],
493
- sessionSelectionIndex: 0,
494
- usedTokensExact: null,
495
- latestOutputTokensPerSecond: null,
496
- modelAutocompleteCandidates: [],
497
- pendingConfirmation: null,
498
- codeContextPath: null,
499
- codeContextMessage: null
500
- };
501
- }
502
- function composeChatRequestMessages(history, codeContextMessage) {
503
- if (!codeContextMessage) {
504
- return history;
505
- }
506
- return [{ role: "system", content: codeContextMessage }, ...history];
507
- }
508
- function buildSubagentScopeMessage(call) {
509
- const lines = [
510
- "Subagent scope:",
511
- `Task: ${call.task}`,
512
- call.context ? `Context: ${call.context}` : null,
513
- call.allowedTools
514
- ? `Allowed tools: ${call.allowedTools.length > 0 ? call.allowedTools.join(", ") : "(none)"}`
515
- : "Allowed tools: all available tools",
516
- "Stay focused on the delegated scope and return concise findings."
517
- ].filter((line) => line !== null);
518
- return lines.join("\n");
519
- }
520
- function buildModelAutocompleteCandidates(modelIds) {
521
- const candidates = [];
522
- const seenValues = new Set();
523
- for (const rawModelId of modelIds) {
524
- const value = rawModelId.trim();
525
- if (value.length === 0 || seenValues.has(value)) {
526
- continue;
527
- }
528
- seenValues.add(value);
529
- const aliases = [];
530
- const parentPath = value.includes("/") ? value.slice(0, value.lastIndexOf("/")) : "";
531
- if (parentPath.length > 0) {
532
- aliases.push(parentPath);
533
- }
534
- const segments = value.split("/").filter((segment) => segment.length > 0);
535
- if (segments.length >= 2) {
536
- aliases.push(`${segments[0]}/${segments[1]}`);
537
- }
538
- const filename = (0, node_path_1.basename)(value);
539
- if (filename.length > 0) {
540
- aliases.push(filename);
541
- if (filename.toLowerCase().endsWith(".gguf")) {
542
- aliases.push(filename.slice(0, -5));
543
- }
544
- }
545
- const dedupedAliases = [
546
- ...new Set(aliases.filter((alias) => alias.length > 0 && alias !== value))
547
- ];
548
- candidates.push({
549
- value,
550
- aliases: dedupedAliases
551
- });
552
- }
553
- return candidates;
554
- }
555
- function withCursorAt(content, index) {
556
- const chars = Array.from(content);
557
- if (chars.length === 0) {
558
- return content;
559
- }
560
- const safeIndex = Math.max(0, Math.min(index, chars.length - 1));
561
- chars[safeIndex] = CURSOR_MARKER;
562
- return chars.join("");
563
- }
564
- const MAX_AUTOCOMPLETE_PREVIEW = 8;
565
- function withReverseHighlight(text) {
566
- return `${ANSI_REVERSE_ON}${text}${ANSI_RESET_ALL}`;
567
- }
568
- function shouldConsumeSubmitForAutocomplete(menu) {
569
- if (!menu) {
570
- return false;
571
- }
572
- const selected = menu.options[menu.selectedIndex];
573
- if (!selected) {
574
- return false;
575
- }
576
- return selected !== menu.token;
577
- }
578
- function computeVisibleLayoutSlices(rows, titleLines, outputLines, promptLines, outputScrollOffset = 0) {
579
- const safeRows = Math.max(1, rows);
580
- const promptCount = Math.min(promptLines.length, safeRows);
581
- const visiblePrompt = promptLines.slice(-promptCount);
582
- const upperRowCount = Math.max(0, safeRows - visiblePrompt.length);
583
- const renderWidth = inferRenderWidth(titleLines, visiblePrompt);
584
- if (upperRowCount === 0) {
585
- return {
586
- titleLines: [],
587
- outputLines: [],
588
- promptLines: visiblePrompt
589
- };
590
- }
591
- // Output should first consume only the empty space between title and prompt.
592
- // Once that gap is exhausted, additional output lines start pushing the title up.
593
- const baseHiddenTitle = Math.max(0, titleLines.length - upperRowCount);
594
- const initiallyVisibleTitleCount = titleLines.length - baseHiddenTitle;
595
- const maxOffset = computeUsefulOutputScrollCapRows({
596
- rows: safeRows,
597
- titleLines,
598
- outputLines,
599
- promptLines: visiblePrompt,
600
- width: renderWidth
601
- });
602
- const clampedOffset = Math.max(0, Math.min(outputScrollOffset, maxOffset));
603
- const isAtTopOfScrollback = maxOffset > 0 && clampedOffset === maxOffset;
604
- const reservedTitleGap = isAtTopOfScrollback || initiallyVisibleTitleCount <= 0 ? 0 : TITLE_OUTPUT_GAP_ROWS;
605
- const initialGap = Math.max(0, upperRowCount - initiallyVisibleTitleCount - reservedTitleGap);
606
- const scrollWindow = dropTrailingByDisplayRows(outputLines, clampedOffset, renderWidth);
607
- const firstContentIndex = scrollWindow.findIndex((line) => !isVisuallyEmptyLine(line));
608
- const contentWindow = firstContentIndex === -1 ? [] : scrollWindow.slice(firstContentIndex);
609
- let lastContentIndex = -1;
610
- for (let index = contentWindow.length - 1; index >= 0; index -= 1) {
611
- if (!isVisuallyEmptyLine(contentWindow[index] ?? "")) {
612
- lastContentIndex = index;
613
- break;
614
- }
615
- }
616
- const pressureWindow = lastContentIndex === -1 ? [] : contentWindow.slice(0, lastContentIndex + 1);
617
- const trailingSpacerRows = lastContentIndex === -1 ? [] : contentWindow.slice(lastContentIndex + 1);
618
- const outputCount = countDisplayRows(pressureWindow, renderWidth);
619
- const outputConsumedByGap = isAtTopOfScrollback ? 0 : Math.min(outputCount, initialGap);
620
- const outputAfterGap = outputCount - outputConsumedByGap;
621
- const outputConsumedByTitle = isAtTopOfScrollback ? 0 : Math.min(outputAfterGap, initiallyVisibleTitleCount);
622
- const totalHiddenTitle = baseHiddenTitle + outputConsumedByTitle;
623
- const visibleTitle = titleLines.slice(totalHiddenTitle);
624
- const hiddenOutputRows = isAtTopOfScrollback
625
- ? 0
626
- : Math.max(0, outputAfterGap - initiallyVisibleTitleCount);
627
- const visibleCoreOutput = dropLeadingByDisplayRows(pressureWindow, hiddenOutputRows, renderWidth);
628
- const topModeGapRows = isAtTopOfScrollback && visibleTitle.length > 0 ? TITLE_OUTPUT_GAP_ROWS : 0;
629
- const outputRowsAvailable = Math.max(0, upperRowCount - visibleTitle.length - topModeGapRows);
630
- const visibleOutput = [...visibleCoreOutput];
631
- let usedOutputRows = countDisplayRows(visibleOutput, renderWidth);
632
- for (const spacerRow of trailingSpacerRows) {
633
- const nextRows = usedOutputRows + lineDisplayRows(spacerRow, renderWidth);
634
- if (nextRows > outputRowsAvailable) {
635
- break;
636
- }
637
- visibleOutput.push(spacerRow);
638
- usedOutputRows = nextRows;
639
- }
640
- if (usedOutputRows < outputRowsAvailable) {
641
- const outputPadding = new Array(outputRowsAvailable - usedOutputRows).fill("");
642
- if (isAtTopOfScrollback) {
643
- const titleGapPadding = new Array(topModeGapRows).fill("");
644
- return {
645
- titleLines: visibleTitle,
646
- outputLines: [...titleGapPadding, ...visibleOutput, ...outputPadding],
647
- promptLines: visiblePrompt
648
- };
649
- }
650
- return {
651
- titleLines: visibleTitle,
652
- outputLines: [...outputPadding, ...visibleOutput],
653
- promptLines: visiblePrompt
654
- };
655
- }
656
- if (usedOutputRows > outputRowsAvailable) {
657
- const trimmedOutput = dropLeadingByDisplayRows(visibleOutput, usedOutputRows - outputRowsAvailable, renderWidth);
658
- if (isAtTopOfScrollback) {
659
- const titleGapPadding = new Array(topModeGapRows).fill("");
660
- return {
661
- titleLines: visibleTitle,
662
- outputLines: [...titleGapPadding, ...trimmedOutput],
663
- promptLines: visiblePrompt
664
- };
665
- }
666
- return {
667
- titleLines: visibleTitle,
668
- outputLines: trimmedOutput,
669
- promptLines: visiblePrompt
670
- };
671
- }
672
- if (isAtTopOfScrollback) {
673
- const titleGapPadding = new Array(topModeGapRows).fill("");
674
- return {
675
- titleLines: visibleTitle,
676
- outputLines: [...titleGapPadding, ...visibleOutput],
677
- promptLines: visiblePrompt
678
- };
679
- }
680
- return {
681
- titleLines: visibleTitle,
682
- outputLines: visibleOutput,
683
- promptLines: visiblePrompt
684
- };
685
- }
686
- function computeTitleVisibleScrollCap(rows, titleLines, outputLines, promptLines) {
687
- const safeRows = Math.max(1, rows);
688
- const promptCount = Math.min(promptLines.length, safeRows);
689
- const visiblePrompt = promptLines.slice(-promptCount);
690
- const renderWidth = inferRenderWidth(titleLines, visiblePrompt);
691
- return computeUsefulOutputScrollCapRows({
692
- rows: safeRows,
693
- titleLines,
694
- outputLines,
695
- promptLines: visiblePrompt,
696
- width: renderWidth
697
- });
698
- }
699
- function buildAutocompleteOverlayLines(composer, registry) {
700
- const menu = composer.getAutocompleteMenuState();
701
- if (!menu) {
702
- return [];
703
- }
704
- const descriptorBySlashName = new Map(registry.listCommands().map((descriptor) => [`/${descriptor.name}`, descriptor]));
705
- const lines = [];
706
- const windowSize = Math.min(MAX_AUTOCOMPLETE_PREVIEW, menu.options.length);
707
- const startIndex = Math.max(0, Math.min(menu.selectedIndex - Math.floor(windowSize / 2), menu.options.length - windowSize));
708
- const visibleOptions = menu.options.slice(startIndex, startIndex + windowSize);
709
- const commandColumnWidth = Math.max(10, ...visibleOptions.map((option) => charLength(option)));
710
- // Align the command slash with the slash in the prompt row: "│>>> /..."
711
- const leftPadding = " ".repeat(1 + charLength(PROMPT_PREFIX));
712
- for (let rowIndex = 0; rowIndex < visibleOptions.length; rowIndex++) {
713
- const option = visibleOptions[rowIndex] ?? "";
714
- const descriptor = descriptorBySlashName.get(option);
715
- const description = descriptor?.description ?? (option.startsWith("/") ? "Command" : "Local model");
716
- const commandColor = descriptor?.kind === "skill" ? colors_1.INPUT_PINK : colors_1.GRADIENT_BLUE;
717
- const selected = startIndex + rowIndex === menu.selectedIndex;
718
- const paddedCommand = option.padEnd(commandColumnWidth, " ");
719
- const styledCommand = (0, colors_1.colorText)(paddedCommand, commandColor);
720
- const styledDescription = (0, colors_1.horizontalGradient)(description, colors_1.GRADIENT_PINK, colors_1.GRADIENT_YELLOW);
721
- const rowText = `${leftPadding}${styledCommand} ${styledDescription}`;
722
- lines.push(selected ? withReverseHighlight(rowText) : rowText);
723
- }
724
- return lines;
725
- }
726
- function buildPromptRenderLines(width, statusText, promptLayout, showCursor = true) {
727
- const frame = (0, prompt_box_1.buildPromptBoxFrame)(width, statusText, promptLayout.rowCount);
728
- const lines = [(0, colors_1.horizontalGradient)(frame.top, colors_1.GRADIENT_PINK, colors_1.GRADIENT_YELLOW)];
729
- for (let rowIndex = 0; rowIndex < frame.middleRows.length; rowIndex++) {
730
- if (width <= 1) {
731
- lines.push((0, colors_1.horizontalGradient)(frame.middleRows[rowIndex] ?? "", colors_1.GRADIENT_PINK, colors_1.GRADIENT_YELLOW));
732
- continue;
733
- }
734
- const prefix = rowIndex === 0 ? promptLayout.prefix : "";
735
- const contentChars = Array.from(`${prefix}${promptLayout.rows[rowIndex] ?? ""}`).slice(0, frame.innerWidth);
736
- while (contentChars.length < frame.innerWidth) {
737
- contentChars.push(" ");
738
- }
739
- let plainInner = contentChars.join("");
740
- if (showCursor && rowIndex === promptLayout.cursorRow && frame.innerWidth > 0) {
741
- const cursorOffset = rowIndex === 0 ? charLength(prefix) : 0;
742
- const cursorIndex = Math.max(0, Math.min(frame.innerWidth - 1, cursorOffset + promptLayout.cursorCol));
743
- plainInner = withCursorAt(plainInner, cursorIndex);
744
- }
745
- const leftBorder = (0, colors_1.colorText)("│", colors_1.GRADIENT_PINK);
746
- const rightBorder = (0, colors_1.colorText)("│", colors_1.GRADIENT_YELLOW);
747
- const coloredInner = (0, colors_1.colorText)(plainInner, colors_1.INPUT_PINK);
748
- lines.push(`${leftBorder}${coloredInner}${rightBorder}`);
749
- }
750
- if (width <= 1) {
751
- lines.push((0, colors_1.horizontalGradient)(frame.bottom, colors_1.GRADIENT_PINK, colors_1.GRADIENT_YELLOW));
752
- return lines;
753
- }
754
- const clippedStatus = clipPromptStatusText(statusText, frame.innerWidth);
755
- const fill = "─".repeat(Math.max(0, frame.innerWidth - charLength(clippedStatus)));
756
- const leftBottom = (0, colors_1.horizontalGradientAtOffset)("╰", colors_1.GRADIENT_PINK, colors_1.GRADIENT_YELLOW, 0, width);
757
- const fillBottom = (0, colors_1.horizontalGradientAtOffset)(fill, colors_1.GRADIENT_PINK, colors_1.GRADIENT_YELLOW, 1, width);
758
- const statusBottom = (0, colors_1.colorText)(clippedStatus, colors_1.GRADIENT_BLUE);
759
- const rightBottom = (0, colors_1.horizontalGradientAtOffset)("╯", colors_1.GRADIENT_PINK, colors_1.GRADIENT_YELLOW, width - 1, width);
760
- lines.push(`${leftBottom}${fillBottom}${statusBottom}${rightBottom}`);
761
- return lines;
762
- }
763
- function formatError(error) {
764
- return error instanceof Error ? error.message : String(error);
765
- }
766
- function isAbortError(error) {
767
- if (!(error instanceof Error)) {
768
- return false;
769
- }
770
- if (error.name === "AbortError") {
771
- return true;
772
- }
773
- return error.message.toLowerCase().includes("aborted");
774
- }
775
- function formatInputAction(action) {
776
- switch (action.type) {
777
- case "insert":
778
- return `insert(${JSON.stringify(action.text)})`;
779
- case "submit":
780
- case "newline":
781
- case "scroll-page-up":
782
- case "scroll-page-down":
783
- case "scroll-line-up":
784
- case "scroll-line-down":
785
- case "backspace":
786
- case "delete":
787
- case "move-left":
788
- case "move-right":
789
- case "move-up":
790
- case "move-down":
791
- case "home":
792
- case "end":
793
- case "cancel":
794
- case "tab":
795
- return action.type;
796
- }
797
- }
798
- function isAmbiguousPlainEnterChunk(sequence, actions) {
799
- if (!actions.some((action) => action.type === "submit")) {
800
- return false;
801
- }
802
- if (actions.some((action) => action.type === "newline")) {
803
- return false;
804
- }
805
- if (sequence.includes("\x1b")) {
806
- return false;
807
- }
808
- const bytes = Array.from(Buffer.from(sequence, "latin1"));
809
- if (bytes.length === 1 && bytes[0] === 0x0d) {
810
- return true;
811
- }
812
- if (bytes.length === 2 && bytes[0] === 0x0d && bytes[1] === 0x0a) {
813
- return true;
814
- }
815
- return false;
816
- }
817
- function applyInputAction(composer, action) {
818
- switch (action.type) {
819
- case "insert":
820
- for (const char of Array.from(action.text)) {
821
- composer.handleKey(char, { isCharacter: true });
822
- }
823
- return null;
824
- case "newline":
825
- return composer.handleKey("CTRL_ENTER");
826
- case "scroll-page-up":
827
- case "scroll-page-down":
828
- case "scroll-line-up":
829
- case "scroll-line-down":
830
- return null;
831
- case "submit":
832
- return composer.handleKey("ENTER");
833
- case "backspace":
834
- return composer.handleKey("BACKSPACE");
835
- case "delete":
836
- return composer.handleKey("DELETE");
837
- case "move-left":
838
- return composer.handleKey("LEFT");
839
- case "move-right":
840
- return composer.handleKey("RIGHT");
841
- case "move-up":
842
- return composer.handleKey("UP");
843
- case "move-down":
844
- return composer.handleKey("DOWN");
845
- case "home":
846
- return composer.handleKey("HOME");
847
- case "end":
848
- return composer.handleKey("END");
849
- case "tab":
850
- return null;
851
- case "cancel":
852
- return { type: "cancel" };
853
- }
854
- }
855
- function createInkApp(ink) {
856
- const { Box, Text, useApp, useStdin, useStdout } = ink;
857
- return function InkApp({ options, version, onRestartRequested }) {
858
- const { exit } = useApp();
859
- const { stdin, isRawModeSupported, setRawMode } = useStdin();
860
- const { stdout } = useStdout();
861
- const [dimensions, setDimensions] = (0, react_1.useState)(() => ({
862
- columns: stdout.columns ?? 80,
863
- rows: stdout.rows ?? 24
864
- }));
865
- const [, setRenderVersion] = (0, react_1.useState)(0);
866
- const stateRef = (0, react_1.useRef)(null);
867
- const registryRef = (0, react_1.useRef)((0, commands_1.createDefaultRegistry)());
868
- const composerRef = (0, react_1.useRef)(null);
869
- const llamaClientRef = (0, react_1.useRef)(null);
870
- const inputEngineRef = (0, react_1.useRef)(new input_engine_1.InputEngine());
871
- const dimensionsRef = (0, react_1.useRef)(dimensions);
872
- const downloaderSearchTimerRef = (0, react_1.useRef)(null);
873
- const downloaderPreloadJobRef = (0, react_1.useRef)(0);
874
- const downloaderSearchInFlightRef = (0, react_1.useRef)(false);
875
- const downloaderPendingQueryRef = (0, react_1.useRef)(null);
876
- const downloaderProgressDirtyRef = (0, react_1.useRef)(false);
877
- const downloaderProgressBufferRef = (0, react_1.useRef)(null);
878
- const downloaderAbortControllerRef = (0, react_1.useRef)(null);
879
- const busySpinnerRef = (0, react_1.useRef)(null);
880
- const vtSessionRef = (0, react_1.useRef)(null);
881
- const confirmResolverRef = (0, react_1.useRef)(null);
882
- const vtEscapePendingRef = (0, react_1.useRef)(false);
883
- const sessionStartHookRanRef = (0, react_1.useRef)(false);
884
- const sessionEndHookRanRef = (0, react_1.useRef)(false);
885
- const forceRender = (0, react_1.useCallback)(() => {
886
- setRenderVersion((value) => value + 1);
887
- }, []);
888
- const startBusyIndicator = (0, react_1.useCallback)((label) => {
889
- const currentState = stateRef.current;
890
- if (!currentState) {
891
- return;
892
- }
893
- if (!busySpinnerRef.current) {
894
- busySpinnerRef.current = new spinner_1.PulsingSpinner(label);
895
- }
896
- busySpinnerRef.current.start(label);
897
- currentState.busy = true;
898
- currentState.busyLabel = label;
899
- forceRender();
900
- }, [forceRender]);
901
- const stopBusyIndicator = (0, react_1.useCallback)(() => {
902
- const currentState = stateRef.current;
903
- if (!currentState) {
904
- return;
905
- }
906
- busySpinnerRef.current?.stop();
907
- currentState.busy = false;
908
- currentState.busyLabel = "";
909
- forceRender();
910
- }, [forceRender]);
911
- dimensionsRef.current = dimensions;
912
- if (!stateRef.current) {
913
- const state = createRuntimeState(options);
914
- stateRef.current = state;
915
- }
916
- const state = stateRef.current;
917
- if (!llamaClientRef.current) {
918
- llamaClientRef.current = new llama_client_1.LlamaClient({
919
- baseUrl: state.config.llamaBaseUrl,
920
- model: state.config.model
921
- });
922
- }
923
- const createComposer = (0, react_1.useCallback)((seedText) => {
924
- const currentState = stateRef.current;
925
- if (!currentState) {
926
- throw new Error("Runtime state is not initialized.");
927
- }
928
- return new prompt_composer_1.PromptComposer({
929
- interiorWidth: Math.max(0, dimensionsRef.current.columns - 2),
930
- history: [...currentState.inputHistory],
931
- commandAutoComplete: registryRef.current.getAutocompleteCommands(),
932
- modelAutoComplete: currentState.modelAutocompleteCandidates,
933
- prefix: PROMPT_PREFIX,
934
- text: seedText
935
- });
936
- }, []);
937
- const getVtSession = (0, react_1.useCallback)(() => {
938
- if (!vtSessionRef.current) {
939
- vtSessionRef.current = new vt_session_1.VirtualTerminalSession();
940
- }
941
- return vtSessionRef.current;
942
- }, []);
943
- const resolvePendingConfirmation = (0, react_1.useCallback)((approved) => {
944
- const currentState = stateRef.current;
945
- const resolver = confirmResolverRef.current;
946
- confirmResolverRef.current = null;
947
- if (!currentState || !resolver) {
948
- return;
949
- }
950
- currentState.pendingConfirmation = null;
951
- currentState.uiMode = "chat";
952
- resolver(approved);
953
- forceRender();
954
- }, [forceRender]);
955
- const requestToolConfirmation = (0, react_1.useCallback)(async (summary, risk) => {
956
- const currentState = stateRef.current;
957
- if (!currentState) {
958
- return false;
959
- }
960
- currentState.pendingConfirmation = {
961
- summary,
962
- destructive: risk.destructive,
963
- outOfZone: risk.outOfZone
964
- };
965
- currentState.uiMode = "confirm";
966
- forceRender();
967
- return await new Promise((resolveApproval) => {
968
- confirmResolverRef.current = resolveApproval;
969
- });
970
- }, [forceRender]);
971
- if (!composerRef.current) {
972
- composerRef.current = createComposer();
973
- }
974
- const refreshModelAutocomplete = (0, react_1.useCallback)(async () => {
975
- const currentState = stateRef.current;
976
- if (!currentState) {
977
- return;
978
- }
979
- const models = await (0, model_manager_1.listLocalModels)({ nicknames: currentState.config.nicknames });
980
- const candidates = buildModelAutocompleteCandidates(models.map((model) => model.id));
981
- const state = stateRef.current;
982
- if (!state) {
983
- return;
984
- }
985
- state.modelAutocompleteCandidates = candidates;
986
- composerRef.current?.setModelAutocompleteCandidates(candidates);
987
- forceRender();
988
- }, [forceRender]);
989
- const refreshSessionActivity = (0, react_1.useCallback)(async () => {
990
- const currentState = stateRef.current;
991
- if (!currentState) {
992
- return;
993
- }
994
- const sessions = await (0, session_store_1.listSessions)();
995
- const state = stateRef.current;
996
- if (!state) {
997
- return;
998
- }
999
- state.sessionList = sessions;
1000
- state.recentActivity = sessions.slice(0, 5).map((session) => session.display);
1001
- if (state.sessionList.length === 0) {
1002
- state.sessionSelectionIndex = 0;
1003
- }
1004
- else {
1005
- state.sessionSelectionIndex = Math.max(0, Math.min(state.sessionSelectionIndex, state.sessionList.length - 1));
1006
- }
1007
- forceRender();
1008
- }, [forceRender]);
1009
- const persistSessionSnapshot = (0, react_1.useCallback)(async () => {
1010
- const currentState = stateRef.current;
1011
- if (!currentState || currentState.history.length === 0) {
1012
- return;
1013
- }
1014
- try {
1015
- if (!currentState.sessionCreated || !currentState.sessionFilePath) {
1016
- const created = await (0, session_store_1.createSessionFileFromHistory)(currentState.history);
1017
- currentState.sessionFilePath = created.path;
1018
- currentState.sessionName = created.sessionName;
1019
- currentState.sessionCreated = true;
1020
- }
1021
- await (0, session_store_1.writeSessionFile)({
1022
- path: currentState.sessionFilePath,
1023
- username: currentState.username,
1024
- history: currentState.history
1025
- });
1026
- await refreshSessionActivity();
1027
- }
1028
- catch (error) {
1029
- appendOutput(currentState, (0, messages_1.formatWarningMessage)(`Session save failed: ${formatError(error)}`));
1030
- appendOutput(currentState, "");
1031
- forceRender();
1032
- }
1033
- }, [forceRender, refreshSessionActivity]);
1034
- const maybeRenderHookFailure = (0, react_1.useCallback)((result) => {
1035
- if (result.status === "ok" || result.status === "skipped") {
1036
- return;
1037
- }
1038
- const currentState = stateRef.current;
1039
- if (!currentState) {
1040
- return;
1041
- }
1042
- appendOutput(currentState, (0, messages_1.formatWarningMessage)((0, hooks_1.formatHookFailure)(result)));
1043
- appendOutput(currentState, "");
1044
- forceRender();
1045
- }, [forceRender]);
1046
- const runConfiguredHook = (0, react_1.useCallback)(async (name, payload, options) => {
1047
- const currentState = stateRef.current;
1048
- const config = currentState?.config ?? state.config;
1049
- const result = await (0, hooks_1.runHook)(config, name, payload, {
1050
- cwd: process.cwd(),
1051
- sessionName: currentState?.sessionName
1052
- });
1053
- if (options?.surfaceFailure !== false) {
1054
- maybeRenderHookFailure(result);
1055
- }
1056
- return result;
1057
- }, [maybeRenderHookFailure, state.config]);
1058
- const runSessionEndHookOnce = (0, react_1.useCallback)(async (reason) => {
1059
- await runOnceGuarded(sessionEndHookRanRef, async () => {
1060
- const currentState = stateRef.current;
1061
- await runConfiguredHook("on-session-end", {
1062
- reason,
1063
- messageCount: currentState?.messageCount ?? 0,
1064
- historyCount: currentState?.history.length ?? 0,
1065
- sessionName: currentState?.sessionName ?? ""
1066
- }, { surfaceFailure: true });
1067
- });
1068
- }, [runConfiguredHook]);
1069
- const finalizeAndExit = (0, react_1.useCallback)((reason, options) => {
1070
- void (async () => {
1071
- const currentState = stateRef.current;
1072
- if (currentState) {
1073
- currentState.running = false;
1074
- }
1075
- await persistSessionSnapshot();
1076
- await runSessionEndHookOnce(reason);
1077
- if (options?.restart) {
1078
- onRestartRequested();
1079
- }
1080
- exit();
1081
- })();
1082
- }, [exit, onRestartRequested, persistSessionSnapshot, runSessionEndHookOnce]);
1083
- const loadSessionIntoState = (0, react_1.useCallback)(async (path) => {
1084
- const currentState = stateRef.current;
1085
- if (!currentState) {
1086
- return;
1087
- }
1088
- try {
1089
- const loaded = await (0, session_store_1.loadSession)(path);
1090
- currentState.history = loaded.history;
1091
- currentState.sessionName = loaded.sessionName;
1092
- currentState.sessionFilePath = loaded.path;
1093
- currentState.sessionCreated = true;
1094
- replayOutputFromHistory(currentState);
1095
- currentState.usedTokensExact = (0, token_counter_1.estimateConversationTokens)(currentState.history);
1096
- currentState.uiMode = "chat";
1097
- composerRef.current = createComposer();
1098
- await refreshSessionActivity();
1099
- forceRender();
1100
- }
1101
- catch (error) {
1102
- appendOutput(currentState, (0, messages_1.formatErrorMessage)(`Load session failed: ${formatError(error)}`));
1103
- appendOutput(currentState, "");
1104
- currentState.uiMode = "chat";
1105
- composerRef.current = createComposer();
1106
- forceRender();
1107
- }
1108
- }, [createComposer, forceRender, refreshSessionActivity]);
1109
- (0, react_1.useEffect)(() => {
1110
- return () => {
1111
- inputEngineRef.current.reset();
1112
- if (downloaderSearchTimerRef.current) {
1113
- clearTimeout(downloaderSearchTimerRef.current);
1114
- downloaderSearchTimerRef.current = null;
1115
- }
1116
- downloaderPendingQueryRef.current = null;
1117
- downloaderProgressDirtyRef.current = false;
1118
- downloaderProgressBufferRef.current = null;
1119
- downloaderAbortControllerRef.current?.abort();
1120
- downloaderAbortControllerRef.current = null;
1121
- void persistSessionSnapshot().catch(() => undefined);
1122
- void runSessionEndHookOnce("unmount").catch(() => undefined);
1123
- void (0, llama_server_1.stopLlamaServer)().catch(() => undefined);
1124
- vtSessionRef.current?.dispose();
1125
- vtSessionRef.current = null;
1126
- };
1127
- }, [persistSessionSnapshot, runSessionEndHookOnce]);
1128
- (0, react_1.useEffect)(() => {
1129
- const session = getVtSession();
1130
- const off = session.onData(() => {
1131
- const currentState = stateRef.current;
1132
- if (currentState?.uiMode === "vt") {
1133
- forceRender();
1134
- }
1135
- });
1136
- return () => {
1137
- off();
1138
- };
1139
- }, [forceRender, getVtSession]);
1140
- (0, react_1.useEffect)(() => {
1141
- void refreshSessionActivity();
1142
- }, [refreshSessionActivity]);
1143
- (0, react_1.useEffect)(() => {
1144
- const currentState = stateRef.current;
1145
- void runOnceGuarded(sessionStartHookRanRef, async () => {
1146
- await runConfiguredHook("on-session-start", {
1147
- backend: currentState?.config.backend ?? "llamacpp",
1148
- model: currentState?.config.model ?? "default",
1149
- configPath: process.env["YIPS_CONFIG_PATH"] ?? null
1150
- }, { surfaceFailure: true });
1151
- });
1152
- }, [runConfiguredHook]);
1153
- (0, react_1.useEffect)(() => {
1154
- void (async () => {
1155
- const currentState = stateRef.current;
1156
- if (!currentState) {
1157
- return;
1158
- }
1159
- const loaded = await (0, code_context_1.loadCodeContext)(process.cwd());
1160
- const state = stateRef.current;
1161
- if (!state) {
1162
- return;
1163
- }
1164
- if (!loaded) {
1165
- state.codeContextPath = null;
1166
- state.codeContextMessage = null;
1167
- return;
1168
- }
1169
- state.codeContextPath = loaded.path;
1170
- state.codeContextMessage = (0, code_context_1.toCodeContextSystemMessage)(loaded);
1171
- if (state.config.verbose) {
1172
- const suffix = loaded.truncated ? " (truncated)" : "";
1173
- appendOutput(state, (0, messages_1.formatDimMessage)(`Loaded CODE.md: ${loaded.path}${suffix}`));
1174
- appendOutput(state, "");
1175
- forceRender();
1176
- }
1177
- })();
1178
- }, [forceRender]);
1179
- (0, react_1.useEffect)(() => {
1180
- void refreshModelAutocomplete();
1181
- }, [refreshModelAutocomplete]);
1182
- (0, react_1.useEffect)(() => {
1183
- const tick = setInterval(() => {
1184
- if (!downloaderProgressDirtyRef.current) {
1185
- return;
1186
- }
1187
- const currentState = stateRef.current;
1188
- if (!currentState ||
1189
- currentState.uiMode !== "downloader" ||
1190
- !currentState.downloader ||
1191
- currentState.downloader.phase !== "downloading") {
1192
- downloaderProgressDirtyRef.current = false;
1193
- downloaderProgressBufferRef.current = null;
1194
- return;
1195
- }
1196
- const pending = downloaderProgressBufferRef.current;
1197
- if (pending && currentState.downloader.download) {
1198
- currentState.downloader = (0, downloader_state_1.updateDownloadProgress)(currentState.downloader, {
1199
- bytesDownloaded: pending.bytesDownloaded,
1200
- totalBytes: pending.totalBytes,
1201
- statusText: formatDownloadStatus({
1202
- bytesDownloaded: pending.bytesDownloaded,
1203
- totalBytes: pending.totalBytes,
1204
- startedAtMs: currentState.downloader.download.startedAtMs
1205
- })
1206
- });
1207
- downloaderProgressBufferRef.current = null;
1208
- }
1209
- downloaderProgressDirtyRef.current = false;
1210
- forceRender();
1211
- }, DOWNLOADER_PROGRESS_RENDER_INTERVAL_MS);
1212
- return () => {
1213
- clearInterval(tick);
1214
- };
1215
- }, [forceRender]);
1216
- (0, react_1.useEffect)(() => {
1217
- const tick = setInterval(() => {
1218
- const currentState = stateRef.current;
1219
- if (!currentState?.busy || !busySpinnerRef.current?.isActive()) {
1220
- return;
1221
- }
1222
- forceRender();
1223
- }, BUSY_SPINNER_RENDER_INTERVAL_MS);
1224
- return () => {
1225
- clearInterval(tick);
1226
- };
1227
- }, [forceRender]);
1228
- (0, react_1.useEffect)(() => {
1229
- const onResize = () => {
1230
- const next = {
1231
- columns: stdout.columns ?? 80,
1232
- rows: stdout.rows ?? 24
1233
- };
1234
- vtSessionRef.current?.resize(Math.max(20, next.columns - 2), Math.max(8, next.rows - 6));
1235
- setDimensions(next);
1236
- };
1237
- stdout.on("resize", onResize);
1238
- return () => {
1239
- stdout.off("resize", onResize);
1240
- };
1241
- }, [stdout]);
1242
- (0, react_1.useEffect)(() => {
1243
- const composer = composerRef.current;
1244
- if (!composer)
1245
- return;
1246
- composer.setInteriorWidth(Math.max(0, dimensions.columns - 2));
1247
- forceRender();
1248
- }, [dimensions.columns, forceRender]);
1249
- (0, react_1.useEffect)(() => {
1250
- if (!isRawModeSupported) {
1251
- return;
1252
- }
1253
- setRawMode(true);
1254
- return () => {
1255
- setRawMode(false);
1256
- };
1257
- }, [isRawModeSupported, setRawMode]);
1258
- (0, react_1.useEffect)(() => {
1259
- if (!stdout.isTTY) {
1260
- return;
1261
- }
1262
- stdout.write(ENABLE_MOUSE_REPORTING);
1263
- return () => {
1264
- stdout.write(DISABLE_MOUSE_REPORTING);
1265
- };
1266
- }, [stdout]);
1267
- const requestAssistantFromLlama = (0, react_1.useCallback)(async (options) => {
1268
- const currentState = stateRef.current;
1269
- const llamaClient = llamaClientRef.current;
1270
- if (!currentState || !llamaClient) {
1271
- throw new Error("Chat runtime is not initialized.");
1272
- }
1273
- const estimateCompletionTokens = (text) => (0, token_counter_1.estimateConversationTokens)([{ content: text }]);
1274
- const readiness = await (0, llama_server_1.ensureLlamaReady)(currentState.config);
1275
- if (!readiness.ready) {
1276
- throw new Error(readiness.failure
1277
- ? (0, llama_server_1.formatLlamaStartupFailure)(readiness.failure, currentState.config)
1278
- : "llama.cpp is unavailable.");
1279
- }
1280
- llamaClient.setModel(currentState.config.model);
1281
- const history = options?.historyOverride ?? currentState.history;
1282
- const codeContext = options?.codeContextOverride !== undefined
1283
- ? options.codeContextOverride
1284
- : currentState.codeContextMessage;
1285
- const requestMessages = composeChatRequestMessages(history, codeContext ?? null);
1286
- const shouldStream = options?.streamingOverride ?? currentState.config.streaming;
1287
- const busyLabel = options?.busyLabel ?? "Thinking...";
1288
- if (!shouldStream) {
1289
- const startedAtMs = Date.now();
1290
- startBusyIndicator(busyLabel);
1291
- try {
1292
- const result = await llamaClient.chat(requestMessages, currentState.config.model);
1293
- return {
1294
- text: result.text,
1295
- rendered: false,
1296
- totalTokens: result.usage?.totalTokens,
1297
- completionTokens: result.usage?.completionTokens ?? estimateCompletionTokens(result.text),
1298
- generationDurationMs: Date.now() - startedAtMs
1299
- };
1300
- }
1301
- finally {
1302
- stopBusyIndicator();
1303
- }
1304
- }
1305
- const timestamp = new Date();
1306
- let streamText = "";
1307
- let receivedFirstToken = false;
1308
- let streamStartedAtMs = null;
1309
- const blockStart = currentState.outputLines.length;
1310
- let blockLength = 0;
1311
- startBusyIndicator(busyLabel);
1312
- forceRender();
1313
- try {
1314
- const streamResult = await llamaClient.streamChat(requestMessages, {
1315
- onToken: (token) => {
1316
- if (!receivedFirstToken) {
1317
- receivedFirstToken = true;
1318
- streamStartedAtMs = Date.now();
1319
- stopBusyIndicator();
1320
- }
1321
- streamText += token;
1322
- blockLength = replaceOutputBlock(currentState, blockStart, blockLength, (0, messages_1.formatAssistantMessage)(streamText, timestamp));
1323
- forceRender();
1324
- }
1325
- }, currentState.config.model);
1326
- streamText = streamResult.text;
1327
- if (streamText.length === 0) {
1328
- stopBusyIndicator();
1329
- throw new Error("Streaming response ended without assistant content.");
1330
- }
1331
- return {
1332
- text: streamText,
1333
- rendered: true,
1334
- totalTokens: streamResult.usage?.totalTokens,
1335
- completionTokens: streamResult.usage?.completionTokens ?? estimateCompletionTokens(streamText),
1336
- generationDurationMs: streamStartedAtMs === null ? undefined : Math.max(0, Date.now() - streamStartedAtMs)
1337
- };
1338
- }
1339
- catch {
1340
- stopBusyIndicator();
1341
- appendOutput(currentState, (0, messages_1.formatWarningMessage)("Streaming failed. Retrying without streaming."));
1342
- startBusyIndicator("Retrying...");
1343
- const retryStartedAtMs = Date.now();
1344
- try {
1345
- const fallbackResult = await llamaClient.chat(requestMessages, currentState.config.model);
1346
- const fallbackText = fallbackResult.text;
1347
- replaceOutputBlock(currentState, blockStart, blockLength, (0, messages_1.formatAssistantMessage)(fallbackText, timestamp));
1348
- return {
1349
- text: fallbackText,
1350
- rendered: true,
1351
- totalTokens: fallbackResult.usage?.totalTokens,
1352
- completionTokens: fallbackResult.usage?.completionTokens ?? estimateCompletionTokens(fallbackText),
1353
- generationDurationMs: Date.now() - retryStartedAtMs
1354
- };
1355
- }
1356
- catch (fallbackError) {
1357
- currentState.outputLines.splice(blockStart, blockLength);
1358
- throw fallbackError;
1359
- }
1360
- finally {
1361
- stopBusyIndicator();
1362
- }
1363
- }
1364
- }, [forceRender, startBusyIndicator, stopBusyIndicator]);
1365
- const preloadConfiguredModel = (0, react_1.useCallback)(async (forceReloadLocal = false) => {
1366
- const currentState = stateRef.current;
1367
- if (!currentState) {
1368
- return;
1369
- }
1370
- if (currentState.config.backend !== "llamacpp") {
1371
- return;
1372
- }
1373
- if (!resolveLoadedModel(currentState.config.model)) {
1374
- return;
1375
- }
1376
- startBusyIndicator(formatModelLoadingLabel(currentState.config, currentState.config.nicknames));
1377
- try {
1378
- if (forceReloadLocal && (0, llama_server_1.isLocalLlamaEndpoint)(currentState.config)) {
1379
- const resetResult = await (0, llama_server_1.resetLlamaForFreshSession)(currentState.config);
1380
- if (resetResult.failure) {
1381
- throw new Error((0, llama_server_1.formatLlamaStartupFailure)(resetResult.failure, currentState.config));
1382
- }
1383
- return;
1384
- }
1385
- const readyResult = await (0, llama_server_1.ensureLlamaReady)(currentState.config);
1386
- if (!readyResult.ready) {
1387
- throw new Error(readyResult.failure
1388
- ? (0, llama_server_1.formatLlamaStartupFailure)(readyResult.failure, currentState.config)
1389
- : "llama.cpp is unavailable.");
1390
- }
1391
- }
1392
- finally {
1393
- stopBusyIndicator();
1394
- }
1395
- }, [startBusyIndicator, stopBusyIndicator]);
1396
- const assessToolCallRisk = (0, react_1.useCallback)((call, workingZone) => {
1397
- if (call.name === "run_command") {
1398
- const command = typeof call.arguments["command"] === "string" ? call.arguments["command"] : "";
1399
- const cwdArg = typeof call.arguments["cwd"] === "string" ? call.arguments["cwd"] : ".";
1400
- const resolvedCwd = (0, tool_safety_1.resolveToolPath)(cwdArg, workingZone);
1401
- return (0, tool_safety_1.assessCommandRisk)(command, resolvedCwd, workingZone);
1402
- }
1403
- const pathArg = typeof call.arguments["path"] === "string" ? call.arguments["path"] : ".";
1404
- return (0, tool_safety_1.assessPathRisk)(pathArg, workingZone);
1405
- }, []);
1406
- const executeToolCalls = (0, react_1.useCallback)(async (toolCalls) => {
1407
- const currentState = stateRef.current;
1408
- if (!currentState) {
1409
- return [];
1410
- }
1411
- const workingZone = process.cwd();
1412
- const results = [];
1413
- for (const call of toolCalls) {
1414
- const risk = assessToolCallRisk(call, workingZone);
1415
- if (risk.requiresConfirmation) {
1416
- const approved = await requestToolConfirmation(`${call.name} (${call.id})`, risk);
1417
- if (!approved) {
1418
- const deniedResult = {
1419
- callId: call.id,
1420
- tool: call.name,
1421
- status: "denied",
1422
- output: "Action denied by user confirmation policy."
1423
- };
1424
- results.push(deniedResult);
1425
- if (currentState.config.verbose) {
1426
- appendOutput(currentState, (0, messages_1.formatDimMessage)(`[tool] ${call.name} (${call.id}) denied`));
1427
- }
1428
- continue;
1429
- }
1430
- }
1431
- if (currentState.config.verbose) {
1432
- appendOutput(currentState, (0, messages_1.formatDimMessage)(`[tool] ${call.name} (${call.id})`));
1433
- }
1434
- const result = await (0, tool_executor_1.executeToolCall)(call, {
1435
- workingDirectory: workingZone,
1436
- vtSession: getVtSession(),
1437
- runHook: async (name, payload) => await runConfiguredHook(name, payload, { surfaceFailure: false })
1438
- });
1439
- results.push(result);
1440
- if (currentState.config.verbose) {
1441
- appendOutput(currentState, (0, messages_1.formatDimMessage)(`[tool-result] ${result.tool} => ${result.status}`));
1442
- }
1443
- }
1444
- if (currentState.config.verbose) {
1445
- appendOutput(currentState, "");
1446
- }
1447
- return results;
1448
- }, [assessToolCallRisk, getVtSession, requestToolConfirmation]);
1449
- const executeSubagentCalls = (0, react_1.useCallback)(async (subagentCalls) => {
1450
- const currentState = stateRef.current;
1451
- if (!currentState) {
1452
- return [];
1453
- }
1454
- const results = [];
1455
- for (const subagentCall of subagentCalls) {
1456
- if (currentState.config.verbose) {
1457
- appendOutput(currentState, (0, messages_1.formatDimMessage)(`[subagent] spawn ${subagentCall.id}: ${subagentCall.task}`));
1458
- }
1459
- const scopedHistory = [
1460
- { role: "system", content: buildSubagentScopeMessage(subagentCall) },
1461
- { role: "user", content: subagentCall.task }
1462
- ];
1463
- const warnings = [];
1464
- const allowedTools = subagentCall.allowedTools !== undefined ? new Set(subagentCall.allowedTools) : null;
1465
- const startedAtMs = Date.now();
1466
- try {
1467
- const turn = await (0, conductor_1.runConductorTurn)({
1468
- history: scopedHistory,
1469
- requestAssistant: () => requestAssistantFromLlama({
1470
- streamingOverride: false,
1471
- historyOverride: scopedHistory,
1472
- codeContextOverride: null,
1473
- busyLabel: `Subagent ${subagentCall.id}...`
1474
- }),
1475
- executeToolCalls: async (toolCalls) => {
1476
- if (!allowedTools) {
1477
- return executeToolCalls(toolCalls);
1478
- }
1479
- const permittedCalls = [];
1480
- const deniedResults = [];
1481
- for (const call of toolCalls) {
1482
- if (allowedTools.has(call.name)) {
1483
- permittedCalls.push(call);
1484
- continue;
1485
- }
1486
- deniedResults.push({
1487
- callId: call.id,
1488
- tool: call.name,
1489
- status: "denied",
1490
- output: `Tool '${call.name}' is not allowed for subagent ${subagentCall.id}.`
1491
- });
1492
- }
1493
- const permittedResults = permittedCalls.length > 0 ? await executeToolCalls(permittedCalls) : [];
1494
- return [...deniedResults, ...permittedResults];
1495
- },
1496
- onAssistantText: () => {
1497
- // Subagent text is consumed internally and summarized in result metadata.
1498
- },
1499
- onWarning: (message) => {
1500
- warnings.push(message);
1501
- },
1502
- onRoundComplete: () => {
1503
- forceRender();
1504
- },
1505
- estimateCompletionTokens: (text) => (0, token_counter_1.estimateConversationTokens)([{ content: text }]),
1506
- estimateHistoryTokens: (history) => (0, token_counter_1.estimateConversationTokens)(history),
1507
- computeTokensPerSecond,
1508
- maxRounds: subagentCall.maxRounds ?? 4
1509
- });
1510
- const lastAssistant = [...scopedHistory]
1511
- .reverse()
1512
- .find((entry) => entry.role === "assistant")?.content;
1513
- const result = {
1514
- callId: subagentCall.id,
1515
- status: turn.finished ? "ok" : "timeout",
1516
- output: lastAssistant ?? "Subagent completed without assistant output.",
1517
- metadata: {
1518
- rounds: turn.rounds,
1519
- durationMs: Math.max(0, Date.now() - startedAtMs),
1520
- warnings
1521
- }
1522
- };
1523
- results.push(result);
1524
- if (currentState.config.verbose) {
1525
- appendOutput(currentState, (0, messages_1.formatDimMessage)(`[subagent-result] ${subagentCall.id} => ${result.status}`));
1526
- }
1527
- }
1528
- catch (error) {
1529
- const result = {
1530
- callId: subagentCall.id,
1531
- status: "error",
1532
- output: `Subagent failed: ${formatError(error)}`
1533
- };
1534
- results.push(result);
1535
- if (currentState.config.verbose) {
1536
- appendOutput(currentState, (0, messages_1.formatDimMessage)(`[subagent-result] ${subagentCall.id} => error`));
1537
- }
1538
- }
1539
- }
1540
- if (currentState.config.verbose) {
1541
- appendOutput(currentState, "");
1542
- }
1543
- return results;
1544
- }, [executeToolCalls, forceRender, requestAssistantFromLlama]);
1545
- const loadDownloaderModels = (0, react_1.useCallback)(async (tab, query, options) => {
1546
- const currentState = stateRef.current;
1547
- if (!currentState || !currentState.downloader) {
1548
- return null;
1549
- }
1550
- const normalizedQuery = query.trim();
1551
- if (options?.useCache !== false) {
1552
- const cached = (0, downloader_state_1.getCachedModels)(currentState.downloader, tab, normalizedQuery);
1553
- if (cached) {
1554
- if (currentState.downloader.tab === tab) {
1555
- currentState.downloader = (0, downloader_state_1.setModels)(currentState.downloader, cached);
1556
- forceRender();
1557
- }
1558
- return cached;
1559
- }
1560
- }
1561
- if (options?.showLoading !== false && currentState.downloader.tab === tab) {
1562
- currentState.downloader = (0, downloader_state_1.setLoadingModels)(currentState.downloader, "Loading models from Hugging Face...");
1563
- forceRender();
1564
- }
1565
- try {
1566
- const models = await (0, model_downloader_1.listGgufModels)({
1567
- query: normalizedQuery,
1568
- sort: (0, downloader_state_1.tabToSort)(tab),
1569
- limit: 100,
1570
- totalMemoryGb: currentState.downloader.totalMemoryGb
1571
- });
1572
- const state = stateRef.current;
1573
- if (!state || !state.downloader) {
1574
- return null;
1575
- }
1576
- if (state.downloader.cacheQuery !== normalizedQuery) {
1577
- return null;
1578
- }
1579
- let next = (0, downloader_state_1.setCachedModels)(state.downloader, tab, normalizedQuery, models);
1580
- if (state.downloader.tab === tab &&
1581
- state.downloader.searchQuery.trim() === normalizedQuery) {
1582
- next = (0, downloader_state_1.setModels)(next, models);
1583
- }
1584
- state.downloader = next;
1585
- forceRender();
1586
- return models;
1587
- }
1588
- catch (error) {
1589
- const state = stateRef.current;
1590
- if (!state || !state.downloader) {
1591
- return null;
1592
- }
1593
- if (state.downloader.cacheQuery !== normalizedQuery) {
1594
- return null;
1595
- }
1596
- if (state.downloader.tab === tab) {
1597
- state.downloader = (0, downloader_state_1.setDownloaderError)(state.downloader, formatError(error));
1598
- forceRender();
1599
- }
1600
- return null;
1601
- }
1602
- }, [forceRender]);
1603
- const preloadDownloaderTabs = (0, react_1.useCallback)(async (query, activeTab) => {
1604
- const currentState = stateRef.current;
1605
- if (!currentState || !currentState.downloader) {
1606
- return;
1607
- }
1608
- const normalizedQuery = query.trim();
1609
- const jobId = ++downloaderPreloadJobRef.current;
1610
- currentState.downloader = (0, downloader_state_1.setPreloadingTabs)(currentState.downloader, true);
1611
- forceRender();
1612
- const otherTabs = downloader_state_1.DOWNLOADER_TABS.filter((tab) => tab !== activeTab);
1613
- await Promise.allSettled(otherTabs.map((tab) => loadDownloaderModels(tab, normalizedQuery, { showLoading: false, useCache: true })));
1614
- const state = stateRef.current;
1615
- if (!state || !state.downloader) {
1616
- return;
1617
- }
1618
- if (jobId !== downloaderPreloadJobRef.current) {
1619
- return;
1620
- }
1621
- if (state.downloader.cacheQuery !== normalizedQuery) {
1622
- return;
1623
- }
1624
- state.downloader = (0, downloader_state_1.setPreloadingTabs)(state.downloader, false);
1625
- forceRender();
1626
- }, [forceRender, loadDownloaderModels]);
1627
- const normalizeDownloaderQuery = (0, react_1.useCallback)((query) => {
1628
- const normalizedQuery = query.trim();
1629
- if (normalizedQuery.length === 0) {
1630
- return "";
1631
- }
1632
- if (charLength(normalizedQuery) < DOWNLOADER_MIN_SEARCH_CHARS) {
1633
- return null;
1634
- }
1635
- return normalizedQuery;
1636
- }, []);
1637
- const refreshDownloaderQuery = (0, react_1.useCallback)(async (query, showLoading) => {
1638
- const state = stateRef.current;
1639
- if (!state || !state.downloader) {
1640
- return;
1641
- }
1642
- const normalizedQuery = query.trim();
1643
- state.downloader = (0, downloader_state_1.resetModelCache)(state.downloader, normalizedQuery);
1644
- if (showLoading) {
1645
- state.downloader = (0, downloader_state_1.setLoadingModels)(state.downloader, "Loading models from Hugging Face...");
1646
- }
1647
- forceRender();
1648
- await loadDownloaderModels(state.downloader.tab, normalizedQuery, {
1649
- showLoading: false,
1650
- useCache: false
1651
- });
1652
- void preloadDownloaderTabs(normalizedQuery, state.downloader.tab);
1653
- }, [forceRender, loadDownloaderModels, preloadDownloaderTabs]);
1654
- const drainDownloaderSearchQueue = (0, react_1.useCallback)(() => {
1655
- if (downloaderSearchInFlightRef.current) {
1656
- return;
1657
- }
1658
- const run = async () => {
1659
- downloaderSearchInFlightRef.current = true;
1660
- try {
1661
- for (let pendingQuery = downloaderPendingQueryRef.current; pendingQuery !== null; pendingQuery = downloaderPendingQueryRef.current) {
1662
- downloaderPendingQueryRef.current = null;
1663
- await refreshDownloaderQuery(pendingQuery, true);
1664
- }
1665
- }
1666
- finally {
1667
- downloaderSearchInFlightRef.current = false;
1668
- }
1669
- };
1670
- void run();
1671
- }, [refreshDownloaderQuery]);
1672
- const scheduleDownloaderSearch = (0, react_1.useCallback)((query, immediate) => {
1673
- const state = stateRef.current;
1674
- if (!state || !state.downloader) {
1675
- return;
1676
- }
1677
- if (downloaderSearchTimerRef.current) {
1678
- clearTimeout(downloaderSearchTimerRef.current);
1679
- downloaderSearchTimerRef.current = null;
1680
- }
1681
- const normalizedQuery = normalizeDownloaderQuery(query);
1682
- if (normalizedQuery === null) {
1683
- downloaderPendingQueryRef.current = null;
1684
- state.downloader = {
1685
- ...(0, downloader_state_1.resetModelCache)(state.downloader, query),
1686
- phase: "idle",
1687
- loading: false
1688
- };
1689
- forceRender();
1690
- return;
1691
- }
1692
- downloaderPendingQueryRef.current = normalizedQuery;
1693
- if (immediate) {
1694
- drainDownloaderSearchQueue();
1695
- return;
1696
- }
1697
- downloaderSearchTimerRef.current = setTimeout(() => {
1698
- drainDownloaderSearchQueue();
1699
- }, DOWNLOADER_SEARCH_DEBOUNCE_MS);
1700
- }, [drainDownloaderSearchQueue, forceRender, normalizeDownloaderQuery]);
1701
- const syncDownloaderSearchFromComposer = (0, react_1.useCallback)((debounced) => {
1702
- const currentState = stateRef.current;
1703
- const composer = composerRef.current;
1704
- if (!currentState || !currentState.downloader || !composer) {
1705
- return;
1706
- }
1707
- const searchQuery = composer.getText();
1708
- const previousQuery = currentState.downloader.searchQuery;
1709
- if (searchQuery === previousQuery) {
1710
- return;
1711
- }
1712
- currentState.downloader = {
1713
- ...currentState.downloader,
1714
- searchQuery
1715
- };
1716
- scheduleDownloaderSearch(searchQuery, !debounced);
1717
- }, [scheduleDownloaderSearch]);
1718
- const loadDownloaderFiles = (0, react_1.useCallback)(async (repoId) => {
1719
- const currentState = stateRef.current;
1720
- if (!currentState || !currentState.downloader) {
1721
- return;
1722
- }
1723
- currentState.downloader = (0, downloader_state_1.setLoadingFiles)(currentState.downloader, "Loading files...");
1724
- forceRender();
1725
- try {
1726
- const files = await (0, model_downloader_1.listModelFiles)(repoId, {
1727
- totalMemoryGb: currentState.downloader.totalMemoryGb
1728
- });
1729
- const state = stateRef.current;
1730
- if (!state || !state.downloader) {
1731
- return;
1732
- }
1733
- state.downloader = (0, downloader_state_1.setFiles)(state.downloader, repoId, files);
1734
- }
1735
- catch (error) {
1736
- const state = stateRef.current;
1737
- if (!state || !state.downloader) {
1738
- return;
1739
- }
1740
- state.downloader = (0, downloader_state_1.setDownloaderError)(state.downloader, formatError(error));
1741
- }
1742
- forceRender();
1743
- }, [forceRender]);
1744
- const downloadFromDownloaderSelection = (0, react_1.useCallback)(async () => {
1745
- const currentState = stateRef.current;
1746
- if (!currentState || !currentState.downloader) {
1747
- return;
1748
- }
1749
- const file = currentState.downloader.files[currentState.downloader.selectedFileIndex];
1750
- const repoId = currentState.downloader.selectedRepoId;
1751
- if (!file || repoId.trim().length === 0) {
1752
- return;
1753
- }
1754
- if (!file.canRun) {
1755
- currentState.downloader = (0, downloader_state_1.setDownloaderError)(currentState.downloader, `Cannot download selected file: ${file.reason}`);
1756
- forceRender();
1757
- return;
1758
- }
1759
- currentState.downloader = (0, downloader_state_1.startDownload)(currentState.downloader, repoId, file.path, `Downloading ${file.path} from ${repoId}...`);
1760
- downloaderProgressDirtyRef.current = false;
1761
- downloaderProgressBufferRef.current = null;
1762
- const abortController = new AbortController();
1763
- downloaderAbortControllerRef.current = abortController;
1764
- forceRender();
1765
- try {
1766
- const result = await (0, model_downloader_1.downloadModelFile)({
1767
- repoId,
1768
- filename: file.path,
1769
- signal: abortController.signal,
1770
- onProgress: ({ bytesDownloaded, totalBytes }) => {
1771
- const state = stateRef.current;
1772
- if (!state || !state.downloader || !state.downloader.download) {
1773
- return;
1774
- }
1775
- downloaderProgressBufferRef.current = {
1776
- bytesDownloaded,
1777
- totalBytes
1778
- };
1779
- downloaderProgressDirtyRef.current = true;
1780
- }
1781
- });
1782
- const state = stateRef.current;
1783
- if (!state || !state.downloader) {
1784
- return;
1785
- }
1786
- appendOutput(state, (0, messages_1.formatDimMessage)(`Downloaded ${file.path} from ${repoId}.\nSaved to: ${result.localPath}\nUse with: /model ${repoId}/${file.path}`));
1787
- appendOutput(state, "");
1788
- state.downloader = (0, downloader_state_1.finishDownload)(state.downloader);
1789
- downloaderProgressDirtyRef.current = false;
1790
- downloaderProgressBufferRef.current = null;
1791
- downloaderAbortControllerRef.current = null;
1792
- void refreshModelAutocomplete();
1793
- }
1794
- catch (error) {
1795
- const state = stateRef.current;
1796
- if (!state || !state.downloader) {
1797
- return;
1798
- }
1799
- if (!isAbortError(error)) {
1800
- state.downloader = (0, downloader_state_1.setDownloaderError)(state.downloader, formatError(error));
1801
- }
1802
- downloaderProgressDirtyRef.current = false;
1803
- downloaderProgressBufferRef.current = null;
1804
- downloaderAbortControllerRef.current = null;
1805
- }
1806
- forceRender();
1807
- }, [forceRender, refreshModelAutocomplete]);
1808
- const refreshModelManagerModels = (0, react_1.useCallback)(async () => {
1809
- const currentState = stateRef.current;
1810
- if (!currentState || !currentState.modelManager) {
1811
- return;
1812
- }
1813
- currentState.modelManager = (0, model_manager_state_1.setModelManagerLoading)(currentState.modelManager, "Loading local models...");
1814
- forceRender();
1815
- try {
1816
- const models = await (0, model_manager_1.listLocalModels)({
1817
- totalMemoryGb: currentState.modelManager.totalMemoryGb,
1818
- nicknames: currentState.config.nicknames
1819
- });
1820
- const state = stateRef.current;
1821
- if (!state || !state.modelManager) {
1822
- return;
1823
- }
1824
- state.modelManager = (0, model_manager_state_1.setModelManagerModels)(state.modelManager, models);
1825
- }
1826
- catch (error) {
1827
- const state = stateRef.current;
1828
- if (!state || !state.modelManager) {
1829
- return;
1830
- }
1831
- state.modelManager = (0, model_manager_state_1.setModelManagerError)(state.modelManager, formatError(error));
1832
- }
1833
- forceRender();
1834
- }, [forceRender]);
1835
- const syncModelManagerSearchFromComposer = (0, react_1.useCallback)(() => {
1836
- const currentState = stateRef.current;
1837
- const composer = composerRef.current;
1838
- if (!currentState || !currentState.modelManager || !composer) {
1839
- return;
1840
- }
1841
- const query = composer.getText();
1842
- if (query === currentState.modelManager.searchQuery) {
1843
- return;
1844
- }
1845
- currentState.modelManager = (0, model_manager_state_1.setModelManagerSearchQuery)(currentState.modelManager, query);
1846
- forceRender();
1847
- }, [forceRender]);
1848
- const handleUserMessage = (0, react_1.useCallback)(async (text) => {
1849
- const currentState = stateRef.current;
1850
- if (!currentState) {
1851
- return;
1852
- }
1853
- currentState.messageCount += 1;
1854
- appendOutput(currentState, (0, messages_1.formatUserMessage)(text));
1855
- currentState.history.push({ role: "user", content: text });
1856
- currentState.usedTokensExact = (0, token_counter_1.estimateConversationTokens)(currentState.history);
1857
- forceRender();
1858
- if (currentState.config.backend !== "llamacpp") {
1859
- const echo = `Echo: ${text}`;
1860
- appendOutput(currentState, (0, messages_1.formatWarningMessage)(`Backend '${currentState.config.backend}' is not implemented yet. Using echo.`));
1861
- appendOutput(currentState, (0, messages_1.formatAssistantMessage)(echo));
1862
- appendOutput(currentState, "");
1863
- currentState.history.push({ role: "assistant", content: echo });
1864
- currentState.usedTokensExact = (0, token_counter_1.estimateConversationTokens)(currentState.history);
1865
- await persistSessionSnapshot();
1866
- forceRender();
1867
- return;
1868
- }
1869
- try {
1870
- const turn = await (0, conductor_1.runConductorTurn)({
1871
- history: currentState.history,
1872
- requestAssistant: () => requestAssistantFromLlama(),
1873
- executeToolCalls,
1874
- executeSubagentCalls,
1875
- onAssistantText: (assistantText, rendered) => {
1876
- if (!rendered) {
1877
- appendOutput(currentState, (0, messages_1.formatAssistantMessage)(assistantText));
1878
- }
1879
- appendOutput(currentState, "");
1880
- },
1881
- onWarning: (message) => {
1882
- appendOutput(currentState, (0, messages_1.formatWarningMessage)(message));
1883
- appendOutput(currentState, "");
1884
- },
1885
- onRoundComplete: () => {
1886
- forceRender();
1887
- },
1888
- estimateCompletionTokens: (text) => (0, token_counter_1.estimateConversationTokens)([{ content: text }]),
1889
- estimateHistoryTokens: (history) => (0, token_counter_1.estimateConversationTokens)(history),
1890
- computeTokensPerSecond
1891
- });
1892
- currentState.latestOutputTokensPerSecond = turn.latestOutputTokensPerSecond;
1893
- currentState.usedTokensExact = turn.usedTokensExact;
1894
- }
1895
- catch (error) {
1896
- appendOutput(currentState, (0, messages_1.formatErrorMessage)(`Request failed: ${formatError(error)}`));
1897
- appendOutput(currentState, "");
1898
- }
1899
- await persistSessionSnapshot();
1900
- forceRender();
1901
- }, [
1902
- executeSubagentCalls,
1903
- executeToolCalls,
1904
- forceRender,
1905
- persistSessionSnapshot,
1906
- requestAssistantFromLlama
1907
- ]);
1908
- const processSubmittedInput = (0, react_1.useCallback)(async (input) => {
1909
- const currentState = stateRef.current;
1910
- if (!currentState) {
1911
- return;
1912
- }
1913
- composerRef.current = createComposer();
1914
- forceRender();
1915
- const trimmed = input.trim();
1916
- if (trimmed.length === 0)
1917
- return;
1918
- currentState.inputHistory.push(trimmed);
1919
- const parsed = (0, commands_1.parseCommand)(trimmed);
1920
- if (parsed) {
1921
- const context = {
1922
- config: currentState.config,
1923
- messageCount: currentState.messageCount
1924
- };
1925
- const result = await registryRef.current.dispatch(parsed.command, parsed.args, context);
1926
- if (parsed.command === "download" || parsed.command === "dl") {
1927
- void refreshModelAutocomplete();
1928
- }
1929
- const suppressModelSetOutput = parsed.command === "model" && parsed.args.trim().length > 0;
1930
- if (result.output && !suppressModelSetOutput) {
1931
- appendOutput(currentState, (0, messages_1.formatDimMessage)(result.output));
1932
- appendOutput(currentState, "");
1933
- }
1934
- if (parsed.command === "model" && parsed.args.trim().length > 0) {
1935
- try {
1936
- pruneModelSwitchStatusArtifacts(currentState);
1937
- await preloadConfiguredModel(true);
1938
- }
1939
- catch (error) {
1940
- appendOutput(currentState, (0, messages_1.formatErrorMessage)(`Model preload failed: ${formatError(error)}`));
1941
- appendOutput(currentState, "");
1942
- }
1943
- }
1944
- if (result.action === "clear") {
1945
- resetSession(currentState);
1946
- await refreshSessionActivity();
1947
- }
1948
- if (result.uiAction?.type === "open-downloader") {
1949
- if (!currentState.downloader) {
1950
- const specs = (0, hardware_1.getSystemSpecs)();
1951
- currentState.downloader = (0, downloader_state_1.createDownloaderState)(specs);
1952
- }
1953
- currentState.uiMode = "downloader";
1954
- composerRef.current = createComposer(currentState.downloader.searchQuery);
1955
- forceRender();
1956
- scheduleDownloaderSearch(currentState.downloader.searchQuery, true);
1957
- return;
1958
- }
1959
- if (result.uiAction?.type === "open-model-manager") {
1960
- if (!currentState.modelManager) {
1961
- const specs = (0, hardware_1.getSystemSpecs)();
1962
- currentState.modelManager = (0, model_manager_state_1.createModelManagerState)(specs);
1963
- }
1964
- currentState.uiMode = "model-manager";
1965
- composerRef.current = createComposer(currentState.modelManager.searchQuery);
1966
- forceRender();
1967
- void refreshModelManagerModels();
1968
- return;
1969
- }
1970
- if (result.uiAction?.type === "open-sessions") {
1971
- await refreshSessionActivity();
1972
- if (currentState.sessionList.length === 0) {
1973
- appendOutput(currentState, (0, messages_1.formatDimMessage)("No session history found."));
1974
- appendOutput(currentState, "");
1975
- forceRender();
1976
- return;
1977
- }
1978
- currentState.uiMode = "sessions";
1979
- currentState.sessionSelectionIndex = 0;
1980
- composerRef.current = createComposer();
1981
- forceRender();
1982
- return;
1983
- }
1984
- if (result.uiAction?.type === "open-vt") {
1985
- currentState.uiMode = "vt";
1986
- getVtSession().ensureStarted(Math.max(20, dimensionsRef.current.columns - 2), Math.max(8, dimensionsRef.current.rows - 6));
1987
- forceRender();
1988
- return;
1989
- }
1990
- forceRender();
1991
- if (result.action === "exit") {
1992
- finalizeAndExit("command-exit");
1993
- }
1994
- if (result.action === "restart") {
1995
- finalizeAndExit("command-restart", { restart: true });
1996
- }
1997
- return;
1998
- }
1999
- await handleUserMessage(trimmed);
2000
- }, [
2001
- createComposer,
2002
- finalizeAndExit,
2003
- forceRender,
2004
- handleUserMessage,
2005
- refreshModelAutocomplete,
2006
- refreshSessionActivity,
2007
- refreshModelManagerModels,
2008
- scheduleDownloaderSearch,
2009
- getVtSession,
2010
- preloadConfiguredModel
2011
- ]);
2012
- const dispatchComposerEvent = (0, react_1.useCallback)((event) => {
2013
- const composer = composerRef.current;
2014
- if (event.type === "submit") {
2015
- void processSubmittedInput(event.value);
2016
- return;
2017
- }
2018
- if (event.type === "cancel") {
2019
- finalizeAndExit("composer-cancel");
2020
- return;
2021
- }
2022
- if (event.type === "autocomplete-menu") {
2023
- const firstOption = event.options[0];
2024
- if (composer && firstOption) {
2025
- composer.applyAutocompleteChoice(event.tokenStart, event.tokenEnd, firstOption);
2026
- }
2027
- }
2028
- forceRender();
2029
- }, [finalizeAndExit, forceRender, processSubmittedInput]);
2030
- (0, react_1.useEffect)(() => {
2031
- const onData = (chunk) => {
2032
- const currentState = stateRef.current;
2033
- const composer = composerRef.current;
2034
- if (!currentState || !composer) {
2035
- return;
2036
- }
2037
- const sequence = Buffer.isBuffer(chunk) ? chunk.toString("latin1") : String(chunk);
2038
- const actions = inputEngineRef.current.pushChunk(chunk);
2039
- if (KEY_DEBUG_ENABLED) {
2040
- const actionSummary = actions.map(formatInputAction).join(", ");
2041
- appendOutput(currentState, (0, messages_1.formatDimMessage)(`[debug stdin] bytes=${toDebugBytes(sequence)} text=${toDebugText(sequence)} actions=[${actionSummary}]`));
2042
- if (isAmbiguousPlainEnterChunk(sequence, actions)) {
2043
- appendOutput(currentState, (0, messages_1.formatWarningMessage)("Terminal emitted plain CR for submit; Ctrl+Enter may be indistinguishable from Enter in this terminal config."));
2044
- }
2045
- forceRender();
2046
- }
2047
- if (actions.length === 0) {
2048
- return;
2049
- }
2050
- if (currentState.uiMode === "confirm") {
2051
- const decision = (0, tui_input_routing_1.decideConfirmationAction)(actions);
2052
- if (decision === "approve") {
2053
- resolvePendingConfirmation(true);
2054
- return;
2055
- }
2056
- if (decision === "deny") {
2057
- resolvePendingConfirmation(false);
2058
- return;
2059
- }
2060
- forceRender();
2061
- return;
2062
- }
2063
- if (currentState.uiMode === "vt") {
2064
- const route = (0, tui_input_routing_1.routeVtInput)(sequence, vtEscapePendingRef.current);
2065
- vtEscapePendingRef.current = route.nextEscapePending;
2066
- if (route.exitToChat) {
2067
- currentState.uiMode = "chat";
2068
- forceRender();
2069
- return;
2070
- }
2071
- if (route.passthrough !== null) {
2072
- getVtSession().write(route.passthrough);
2073
- forceRender();
2074
- }
2075
- return;
2076
- }
2077
- if (currentState.uiMode === "sessions") {
2078
- if (currentState.sessionList.length === 0) {
2079
- currentState.uiMode = "chat";
2080
- forceRender();
2081
- return;
2082
- }
2083
- for (const action of actions) {
2084
- if (action.type === "cancel") {
2085
- currentState.uiMode = "chat";
2086
- forceRender();
2087
- return;
2088
- }
2089
- if (action.type === "move-up") {
2090
- const total = currentState.sessionList.length;
2091
- currentState.sessionSelectionIndex =
2092
- (currentState.sessionSelectionIndex - 1 + total) % total;
2093
- continue;
2094
- }
2095
- if (action.type === "move-down") {
2096
- const total = currentState.sessionList.length;
2097
- currentState.sessionSelectionIndex = (currentState.sessionSelectionIndex + 1) % total;
2098
- continue;
2099
- }
2100
- if (action.type === "submit") {
2101
- const selected = currentState.sessionList[currentState.sessionSelectionIndex];
2102
- if (selected) {
2103
- void loadSessionIntoState(selected.path);
2104
- }
2105
- else {
2106
- currentState.uiMode = "chat";
2107
- forceRender();
2108
- }
2109
- return;
2110
- }
2111
- }
2112
- forceRender();
2113
- return;
2114
- }
2115
- if (currentState.uiMode === "model-manager") {
2116
- const modelManager = currentState.modelManager;
2117
- if (!modelManager) {
2118
- currentState.uiMode = "chat";
2119
- forceRender();
2120
- return;
2121
- }
2122
- for (const action of actions) {
2123
- if (action.type === "cancel") {
2124
- currentState.uiMode = "chat";
2125
- forceRender();
2126
- return;
2127
- }
2128
- if (!currentState.modelManager) {
2129
- continue;
2130
- }
2131
- if (action.type === "insert") {
2132
- if (action.text.toLowerCase() === "t" && composer.getText().trim().length === 0) {
2133
- if (!currentState.downloader) {
2134
- const specs = (0, hardware_1.getSystemSpecs)();
2135
- currentState.downloader = (0, downloader_state_1.createDownloaderState)(specs);
2136
- }
2137
- currentState.uiMode = "downloader";
2138
- composerRef.current = createComposer(currentState.downloader.searchQuery);
2139
- forceRender();
2140
- scheduleDownloaderSearch(currentState.downloader.searchQuery, true);
2141
- return;
2142
- }
2143
- applyInputAction(composer, action);
2144
- syncModelManagerSearchFromComposer();
2145
- continue;
2146
- }
2147
- if (action.type === "backspace" ||
2148
- action.type === "home" ||
2149
- action.type === "end" ||
2150
- action.type === "move-left" ||
2151
- action.type === "move-right") {
2152
- applyInputAction(composer, action);
2153
- syncModelManagerSearchFromComposer();
2154
- continue;
2155
- }
2156
- if (action.type === "delete") {
2157
- const selected = (0, model_manager_state_1.getSelectedModel)(currentState.modelManager);
2158
- if (!selected) {
2159
- continue;
2160
- }
2161
- currentState.modelManager = (0, model_manager_state_1.setModelManagerLoading)(currentState.modelManager, `Deleting ${selected.name}...`);
2162
- forceRender();
2163
- void (async () => {
2164
- const state = stateRef.current;
2165
- if (!state || !state.modelManager) {
2166
- return;
2167
- }
2168
- try {
2169
- await (0, model_manager_1.deleteLocalModel)(selected);
2170
- state.modelManager = (0, model_manager_state_1.removeModelById)(state.modelManager, selected.id);
2171
- if (state.config.model === selected.id) {
2172
- state.config.model = "default";
2173
- await (0, config_1.saveConfig)(state.config);
2174
- }
2175
- void refreshModelAutocomplete();
2176
- }
2177
- catch (error) {
2178
- state.modelManager = (0, model_manager_state_1.setModelManagerError)(state.modelManager, `Delete failed: ${formatError(error)}`);
2179
- }
2180
- forceRender();
2181
- })();
2182
- return;
2183
- }
2184
- if (currentState.modelManager.loading) {
2185
- continue;
2186
- }
2187
- if (action.type === "move-up") {
2188
- currentState.modelManager = (0, model_manager_state_1.moveModelManagerSelection)(currentState.modelManager, -1, 11);
2189
- continue;
2190
- }
2191
- if (action.type === "move-down") {
2192
- currentState.modelManager = (0, model_manager_state_1.moveModelManagerSelection)(currentState.modelManager, 1, 11);
2193
- continue;
2194
- }
2195
- if (action.type === "submit") {
2196
- const selected = (0, model_manager_state_1.getSelectedModel)(currentState.modelManager);
2197
- if (!selected) {
2198
- continue;
2199
- }
2200
- currentState.uiMode = "chat";
2201
- composerRef.current = createComposer();
2202
- forceRender();
2203
- void (async () => {
2204
- const state = stateRef.current;
2205
- if (!state) {
2206
- return;
2207
- }
2208
- state.config.backend = "llamacpp";
2209
- state.config.model = selected.id;
2210
- try {
2211
- await (0, config_1.saveConfig)(state.config);
2212
- pruneModelSwitchStatusArtifacts(state);
2213
- await preloadConfiguredModel(true);
2214
- }
2215
- catch (error) {
2216
- appendOutput(state, (0, messages_1.formatErrorMessage)(`Model preload failed: ${formatError(error)}`));
2217
- appendOutput(state, "");
2218
- }
2219
- forceRender();
2220
- })();
2221
- return;
2222
- }
2223
- }
2224
- forceRender();
2225
- return;
2226
- }
2227
- if (currentState.uiMode === "downloader") {
2228
- const downloader = currentState.downloader;
2229
- if (!downloader) {
2230
- currentState.uiMode = "chat";
2231
- forceRender();
2232
- return;
2233
- }
2234
- for (const action of actions) {
2235
- if (action.type === "cancel") {
2236
- if (currentState.downloader?.phase === "downloading") {
2237
- if (currentState.downloader.cancelConfirmOpen) {
2238
- currentState.downloader = (0, downloader_state_1.closeCancelConfirm)(currentState.downloader);
2239
- }
2240
- else {
2241
- currentState.downloader = (0, downloader_state_1.openCancelConfirm)(currentState.downloader);
2242
- }
2243
- forceRender();
2244
- return;
2245
- }
2246
- if (currentState.downloader?.view === "files") {
2247
- currentState.downloader = (0, downloader_state_1.closeFileView)(currentState.downloader);
2248
- }
2249
- else {
2250
- currentState.uiMode = "chat";
2251
- if (downloaderSearchTimerRef.current) {
2252
- clearTimeout(downloaderSearchTimerRef.current);
2253
- downloaderSearchTimerRef.current = null;
2254
- }
2255
- downloaderPendingQueryRef.current = null;
2256
- }
2257
- forceRender();
2258
- return;
2259
- }
2260
- if (!currentState.downloader) {
2261
- continue;
2262
- }
2263
- if (currentState.downloader.phase === "downloading" &&
2264
- currentState.downloader.cancelConfirmOpen) {
2265
- if (action.type === "submit") {
2266
- downloaderAbortControllerRef.current?.abort();
2267
- downloaderAbortControllerRef.current = null;
2268
- currentState.downloader = (0, downloader_state_1.finishDownload)(currentState.downloader);
2269
- downloaderProgressDirtyRef.current = false;
2270
- downloaderProgressBufferRef.current = null;
2271
- appendOutput(currentState, (0, messages_1.formatDimMessage)("Download canceled."));
2272
- appendOutput(currentState, "");
2273
- forceRender();
2274
- return;
2275
- }
2276
- continue;
2277
- }
2278
- if (currentState.downloader.view === "models") {
2279
- if (action.type === "insert" ||
2280
- action.type === "backspace" ||
2281
- action.type === "delete" ||
2282
- action.type === "home" ||
2283
- action.type === "end") {
2284
- applyInputAction(composer, action);
2285
- syncDownloaderSearchFromComposer(true);
2286
- continue;
2287
- }
2288
- if (currentState.downloader.loading) {
2289
- continue;
2290
- }
2291
- if (action.type === "move-left") {
2292
- currentState.downloader = (0, downloader_state_1.cycleTab)(currentState.downloader, -1);
2293
- const query = currentState.downloader.searchQuery;
2294
- const normalizedQuery = normalizeDownloaderQuery(query);
2295
- if (normalizedQuery === null) {
2296
- forceRender();
2297
- return;
2298
- }
2299
- const cached = (0, downloader_state_1.getCachedModels)(currentState.downloader, currentState.downloader.tab, normalizedQuery);
2300
- if (cached) {
2301
- currentState.downloader = (0, downloader_state_1.setModels)(currentState.downloader, cached);
2302
- forceRender();
2303
- void preloadDownloaderTabs(normalizedQuery, currentState.downloader.tab);
2304
- return;
2305
- }
2306
- forceRender();
2307
- void loadDownloaderModels(currentState.downloader.tab, normalizedQuery, {
2308
- showLoading: true,
2309
- useCache: false
2310
- });
2311
- return;
2312
- }
2313
- if (action.type === "move-right") {
2314
- currentState.downloader = (0, downloader_state_1.cycleTab)(currentState.downloader, 1);
2315
- const query = currentState.downloader.searchQuery;
2316
- const normalizedQuery = normalizeDownloaderQuery(query);
2317
- if (normalizedQuery === null) {
2318
- forceRender();
2319
- return;
2320
- }
2321
- const cached = (0, downloader_state_1.getCachedModels)(currentState.downloader, currentState.downloader.tab, normalizedQuery);
2322
- if (cached) {
2323
- currentState.downloader = (0, downloader_state_1.setModels)(currentState.downloader, cached);
2324
- forceRender();
2325
- void preloadDownloaderTabs(normalizedQuery, currentState.downloader.tab);
2326
- return;
2327
- }
2328
- forceRender();
2329
- void loadDownloaderModels(currentState.downloader.tab, normalizedQuery, {
2330
- showLoading: true,
2331
- useCache: false
2332
- });
2333
- return;
2334
- }
2335
- if (action.type === "move-up") {
2336
- currentState.downloader = (0, downloader_state_1.moveModelSelection)(currentState.downloader, -1, 9);
2337
- continue;
2338
- }
2339
- if (action.type === "move-down") {
2340
- currentState.downloader = (0, downloader_state_1.moveModelSelection)(currentState.downloader, 1, 9);
2341
- continue;
2342
- }
2343
- if (action.type === "submit") {
2344
- const selected = currentState.downloader.models[currentState.downloader.selectedModelIndex];
2345
- if (selected) {
2346
- void loadDownloaderFiles(selected.id);
2347
- }
2348
- return;
2349
- }
2350
- }
2351
- else {
2352
- if (currentState.downloader.loading) {
2353
- continue;
2354
- }
2355
- if (action.type === "move-up") {
2356
- currentState.downloader = (0, downloader_state_1.moveFileSelection)(currentState.downloader, -1, 9);
2357
- continue;
2358
- }
2359
- if (action.type === "move-down") {
2360
- currentState.downloader = (0, downloader_state_1.moveFileSelection)(currentState.downloader, 1, 9);
2361
- continue;
2362
- }
2363
- if (action.type === "submit") {
2364
- void downloadFromDownloaderSelection();
2365
- return;
2366
- }
2367
- }
2368
- }
2369
- forceRender();
2370
- return;
2371
- }
2372
- composer.setInteriorWidth(Math.max(0, dimensionsRef.current.columns - 2));
2373
- let shouldRender = false;
2374
- const computeCurrentScrollCap = () => {
2375
- const promptLayout = composer.getLayout();
2376
- const statusText = buildPromptStatusText(currentState);
2377
- const titleLines = (0, title_box_1.renderTitleBox)(buildTitleBoxOptions(currentState, version, dimensionsRef.current.columns));
2378
- const promptLines = buildPromptRenderLines(dimensionsRef.current.columns, statusText, promptLayout, true);
2379
- const autocompleteOverlay = buildAutocompleteOverlayLines(composer, registryRef.current);
2380
- const busyLine = currentState.busy && busySpinnerRef.current ? busySpinnerRef.current.render() : "";
2381
- const visibleOutputLines = composeOutputLines({
2382
- outputLines: currentState.outputLines,
2383
- autocompleteOverlay,
2384
- busyLine
2385
- });
2386
- return computeTitleVisibleScrollCap(dimensionsRef.current.rows, titleLines, visibleOutputLines, promptLines);
2387
- };
2388
- for (const action of actions) {
2389
- if (action.type === "cancel") {
2390
- finalizeAndExit("input-cancel");
2391
- return;
2392
- }
2393
- if (action.type === "scroll-page-up") {
2394
- const pageSize = Math.max(1, dimensionsRef.current.rows - 6);
2395
- shiftOutputScrollOffsetWithCap(currentState, pageSize, computeCurrentScrollCap());
2396
- shouldRender = true;
2397
- continue;
2398
- }
2399
- if (action.type === "scroll-page-down") {
2400
- const pageSize = Math.max(1, dimensionsRef.current.rows - 6);
2401
- shiftOutputScrollOffsetWithCap(currentState, -pageSize, computeCurrentScrollCap());
2402
- shouldRender = true;
2403
- continue;
2404
- }
2405
- if (action.type === "scroll-line-up") {
2406
- shiftOutputScrollOffsetWithCap(currentState, MOUSE_SCROLL_LINE_STEP, computeCurrentScrollCap());
2407
- shouldRender = true;
2408
- continue;
2409
- }
2410
- if (action.type === "scroll-line-down") {
2411
- shiftOutputScrollOffsetWithCap(currentState, -MOUSE_SCROLL_LINE_STEP, computeCurrentScrollCap());
2412
- shouldRender = true;
2413
- continue;
2414
- }
2415
- if (currentState.busy) {
2416
- continue;
2417
- }
2418
- const menuState = composer.getAutocompleteMenuState();
2419
- if (menuState) {
2420
- if (action.type === "move-up") {
2421
- composer.moveAutocompleteSelection(-1);
2422
- shouldRender = true;
2423
- continue;
2424
- }
2425
- if (action.type === "move-down") {
2426
- composer.moveAutocompleteSelection(1);
2427
- shouldRender = true;
2428
- continue;
2429
- }
2430
- if (action.type === "tab") {
2431
- composer.acceptAutocompleteSelection();
2432
- shouldRender = true;
2433
- continue;
2434
- }
2435
- if (action.type === "submit") {
2436
- if (shouldConsumeSubmitForAutocomplete(menuState)) {
2437
- composer.acceptAutocompleteSelection();
2438
- shouldRender = true;
2439
- continue;
2440
- }
2441
- }
2442
- }
2443
- const event = applyInputAction(composer, action);
2444
- if (!event) {
2445
- shouldRender = true;
2446
- continue;
2447
- }
2448
- if (event.type === "none") {
2449
- shouldRender = true;
2450
- continue;
2451
- }
2452
- dispatchComposerEvent(event);
2453
- return;
2454
- }
2455
- if (shouldRender) {
2456
- forceRender();
2457
- }
2458
- };
2459
- stdin.on("data", onData);
2460
- return () => {
2461
- stdin.off("data", onData);
2462
- };
2463
- }, [
2464
- createComposer,
2465
- dispatchComposerEvent,
2466
- downloadFromDownloaderSelection,
2467
- finalizeAndExit,
2468
- forceRender,
2469
- loadSessionIntoState,
2470
- loadDownloaderFiles,
2471
- loadDownloaderModels,
2472
- normalizeDownloaderQuery,
2473
- preloadDownloaderTabs,
2474
- refreshModelAutocomplete,
2475
- resolvePendingConfirmation,
2476
- syncModelManagerSearchFromComposer,
2477
- syncDownloaderSearchFromComposer,
2478
- getVtSession,
2479
- stdin
2480
- ]);
2481
- const composer = composerRef.current;
2482
- composer.setInteriorWidth(Math.max(0, dimensions.columns - 2));
2483
- let titleNodes = [];
2484
- let outputNodes = [];
2485
- let promptNodes = [];
2486
- const promptLayout = composer.getLayout();
2487
- const statusText = buildPromptStatusText(state);
2488
- const titleLines = (0, title_box_1.renderTitleBox)(buildTitleBoxOptions(state, version, dimensions.columns));
2489
- const promptLines = buildPromptRenderLines(dimensions.columns, statusText, promptLayout, true);
2490
- const autocompleteOverlay = state.uiMode === "downloader" ||
2491
- state.uiMode === "model-manager" ||
2492
- state.uiMode === "sessions" ||
2493
- state.uiMode === "vt" ||
2494
- state.uiMode === "confirm"
2495
- ? []
2496
- : buildAutocompleteOverlayLines(composer, registryRef.current);
2497
- const busyLine = state.busy && busySpinnerRef.current ? busySpinnerRef.current.render() : "";
2498
- const outputLines = composeOutputLines({
2499
- outputLines: state.outputLines,
2500
- autocompleteOverlay,
2501
- busyLine
2502
- });
2503
- if (state.uiMode === "downloader" && state.downloader) {
2504
- outputLines.push("");
2505
- outputLines.push(...(0, downloader_ui_1.renderDownloaderLines)({
2506
- width: dimensions.columns,
2507
- state: state.downloader
2508
- }));
2509
- }
2510
- if (state.uiMode === "model-manager" && state.modelManager) {
2511
- outputLines.push("");
2512
- outputLines.push(...(0, model_manager_ui_1.renderModelManagerLines)({
2513
- width: dimensions.columns,
2514
- state: state.modelManager,
2515
- currentModel: state.config.model
2516
- }));
2517
- }
2518
- if (state.uiMode === "vt") {
2519
- const vtLines = getVtSession().getDisplayLines(Math.max(8, dimensions.rows - 10));
2520
- outputLines.push("");
2521
- outputLines.push((0, colors_1.horizontalGradient)("╭─── Yips Virtual Terminal ───────────────────────────────╮", colors_1.GRADIENT_PINK, colors_1.GRADIENT_YELLOW));
2522
- if (vtLines.length === 0) {
2523
- outputLines.push((0, colors_1.colorText)("│ starting shell... │", colors_1.GRADIENT_BLUE));
2524
- }
2525
- else {
2526
- for (const line of vtLines.slice(-Math.max(1, dimensions.rows - 12))) {
2527
- outputLines.push(line);
2528
- }
2529
- }
2530
- outputLines.push((0, colors_1.colorText)("Esc Esc: return to chat | Ctrl+Q: return to chat", colors_1.GRADIENT_BLUE));
2531
- }
2532
- if (state.uiMode === "confirm" && state.pendingConfirmation) {
2533
- const riskTags = [
2534
- state.pendingConfirmation.destructive ? "destructive" : null,
2535
- state.pendingConfirmation.outOfZone ? "outside-working-zone" : null
2536
- ]
2537
- .filter((value) => value !== null)
2538
- .join(", ");
2539
- outputLines.push("");
2540
- outputLines.push((0, messages_1.formatWarningMessage)("Confirmation required"));
2541
- outputLines.push((0, messages_1.formatDimMessage)(`Action: ${state.pendingConfirmation.summary}`));
2542
- if (riskTags.length > 0) {
2543
- outputLines.push((0, messages_1.formatDimMessage)(`Risk: ${riskTags}`));
2544
- }
2545
- outputLines.push((0, messages_1.formatDimMessage)("Approve? [y/N] (Enter = yes, Esc = no)"));
2546
- }
2547
- const visible = computeVisibleLayoutSlices(dimensions.rows, titleLines, outputLines, promptLines, state.outputScrollOffset);
2548
- titleNodes = visible.titleLines.map((line, index) => react_1.default.createElement(Text, { key: `title-${index}` }, line.length > 0 ? line : " "));
2549
- outputNodes = visible.outputLines.map((line, index) => react_1.default.createElement(Text, { key: `out-${index}` }, line.length > 0 ? line : " "));
2550
- promptNodes = visible.promptLines.map((line, index) => react_1.default.createElement(Text, { key: `prompt-${index}` }, line));
2551
- return react_1.default.createElement(Box, { flexDirection: "column" }, ...titleNodes, ...outputNodes, ...promptNodes);
2552
- };
2553
- }