@sheepbun/yips 0.1.1 → 0.1.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +52 -0
- package/bin/yips.js +15 -0
- package/package.json +21 -128
- package/postinstall.js +50 -0
- package/dist/agent/commands/command-catalog.js +0 -243
- package/dist/agent/commands/commands.js +0 -418
- package/dist/agent/conductor.js +0 -118
- package/dist/agent/context/code-context.js +0 -68
- package/dist/agent/context/memory-store.js +0 -159
- package/dist/agent/context/session-store.js +0 -211
- package/dist/agent/protocol/tool-protocol.js +0 -160
- package/dist/agent/skills/skills.js +0 -327
- package/dist/agent/tools/tool-executor.js +0 -415
- package/dist/agent/tools/tool-safety.js +0 -52
- package/dist/app/index.js +0 -35
- package/dist/app/repl.js +0 -105
- package/dist/app/update-check.js +0 -132
- package/dist/app/version.js +0 -51
- package/dist/code-context.js +0 -68
- package/dist/colors.js +0 -204
- package/dist/command-catalog.js +0 -242
- package/dist/commands.js +0 -350
- package/dist/conductor.js +0 -94
- package/dist/config/config.js +0 -335
- package/dist/config/hooks.js +0 -187
- package/dist/config.js +0 -335
- package/dist/downloader-state.js +0 -302
- package/dist/downloader-ui.js +0 -289
- package/dist/gateway/adapters/discord.js +0 -108
- package/dist/gateway/adapters/formatting.js +0 -96
- package/dist/gateway/adapters/telegram.js +0 -106
- package/dist/gateway/adapters/types.js +0 -2
- package/dist/gateway/adapters/whatsapp.js +0 -124
- package/dist/gateway/auth-policy.js +0 -66
- package/dist/gateway/core.js +0 -87
- package/dist/gateway/headless-conductor.js +0 -328
- package/dist/gateway/message-router.js +0 -23
- package/dist/gateway/rate-limiter.js +0 -48
- package/dist/gateway/runtime/backend-policy.js +0 -18
- package/dist/gateway/runtime/discord-bot.js +0 -104
- package/dist/gateway/runtime/discord-main.js +0 -69
- package/dist/gateway/session-manager.js +0 -77
- package/dist/gateway/types.js +0 -2
- package/dist/hardware.js +0 -92
- package/dist/hooks.js +0 -187
- package/dist/index.js +0 -34
- package/dist/input-engine.js +0 -250
- package/dist/llama-client.js +0 -227
- package/dist/llama-server.js +0 -620
- package/dist/llm/llama-client.js +0 -227
- package/dist/llm/llama-server.js +0 -620
- package/dist/llm/token-counter.js +0 -47
- package/dist/memory-store.js +0 -159
- package/dist/messages.js +0 -59
- package/dist/model-downloader.js +0 -382
- package/dist/model-manager-state.js +0 -118
- package/dist/model-manager-ui.js +0 -194
- package/dist/model-manager.js +0 -190
- package/dist/models/hardware.js +0 -92
- package/dist/models/model-downloader.js +0 -382
- package/dist/models/model-manager.js +0 -190
- package/dist/prompt-box.js +0 -78
- package/dist/prompt-composer.js +0 -498
- package/dist/repl.js +0 -105
- package/dist/session-store.js +0 -211
- package/dist/spinner.js +0 -76
- package/dist/title-box.js +0 -388
- package/dist/token-counter.js +0 -47
- package/dist/tool-executor.js +0 -415
- package/dist/tool-protocol.js +0 -121
- package/dist/tool-safety.js +0 -52
- package/dist/tui/app.js +0 -2553
- package/dist/tui/startup.js +0 -56
- package/dist/tui-input-routing.js +0 -53
- package/dist/tui.js +0 -51
- package/dist/types/app-types.js +0 -2
- package/dist/types.js +0 -2
- package/dist/ui/colors.js +0 -204
- package/dist/ui/downloader/downloader-state.js +0 -302
- package/dist/ui/downloader/downloader-ui.js +0 -289
- package/dist/ui/input/input-engine.js +0 -250
- package/dist/ui/input/tui-input-routing.js +0 -53
- package/dist/ui/input/vt-session.js +0 -168
- package/dist/ui/messages.js +0 -59
- package/dist/ui/model-manager/model-manager-state.js +0 -118
- package/dist/ui/model-manager/model-manager-ui.js +0 -194
- package/dist/ui/prompt/prompt-box.js +0 -78
- package/dist/ui/prompt/prompt-composer.js +0 -498
- package/dist/ui/spinner.js +0 -76
- package/dist/ui/title-box.js +0 -388
- package/dist/ui/tui/app.js +0 -6
- package/dist/ui/tui/autocomplete.js +0 -85
- package/dist/ui/tui/constants.js +0 -18
- package/dist/ui/tui/history.js +0 -29
- package/dist/ui/tui/layout.js +0 -341
- package/dist/ui/tui/runtime-core.js +0 -2584
- package/dist/ui/tui/runtime-utils.js +0 -53
- package/dist/ui/tui/start-tui.js +0 -54
- package/dist/ui/tui/startup.js +0 -56
- package/dist/version.js +0 -51
- package/dist/vt-session.js +0 -168
- 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
|
-
}
|