nervous-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/app.js +2547 -0
  2. package/package.json +42 -0
package/dist/app.js ADDED
@@ -0,0 +1,2547 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/app.tsx
4
+ import { FullScreenBox as FullScreenBox2, useScreenSize, withFullScreen } from "fullscreen-ink";
5
+ import { Box as Box9, Text as Text8, useInput as useInput2 } from "ink";
6
+ import { useEffect as useEffect4, useMemo as useMemo2, useRef as useRef3, useState as useState2 } from "react";
7
+
8
+ // src/command/command_list.ts
9
+ var COMMANDS = [
10
+ { name: "/model", description: "choose a model" },
11
+ { name: "/key", description: "change a model API key" },
12
+ { name: "/session", description: "continue a saved session" },
13
+ { name: "/exit", description: "save and quit" },
14
+ { name: "/restart", description: "restart UI and connection" }
15
+ ];
16
+ function filterCommands(input) {
17
+ if (!input.startsWith("/")) return [];
18
+ return COMMANDS.filter((entry) => entry.name.startsWith(input));
19
+ }
20
+ function isCommand(input, name) {
21
+ return input.trim() === name;
22
+ }
23
+ function isSlashCommand(input) {
24
+ return input.trim().startsWith("/");
25
+ }
26
+
27
+ // src/save/config_writer.ts
28
+ import { readFile, writeFile } from "fs/promises";
29
+
30
+ // src/session/user_data.ts
31
+ var user_data = null;
32
+ function setUserData(data) {
33
+ user_data = data;
34
+ }
35
+ function commitSession() {
36
+ if (user_data) user_data.session_committed = true;
37
+ }
38
+
39
+ // src/save/config_path.ts
40
+ import { join } from "path";
41
+
42
+ // src/systemconfig/file.ts
43
+ var CONFIG_FILE_PATH_DIR = "user_config/";
44
+ var CONFIG_FILE_NAME = "config.json";
45
+ var DEFAULT_CONFIG = {
46
+ api_key: {},
47
+ main_agent_id: 1,
48
+ session: {}
49
+ };
50
+
51
+ // src/save/config_path.ts
52
+ function configFilePath() {
53
+ return join(process.cwd(), CONFIG_FILE_PATH_DIR, CONFIG_FILE_NAME);
54
+ }
55
+ function configDirPath() {
56
+ return join(process.cwd(), CONFIG_FILE_PATH_DIR);
57
+ }
58
+
59
+ // src/save/config_writer.ts
60
+ var WRITE_DEBOUNCE_MS = 50;
61
+ var write_queue = Promise.resolve();
62
+ var debounce_timer = null;
63
+ var pending_resolvers = [];
64
+ function flushDebouncedWrite() {
65
+ if (debounce_timer) {
66
+ clearTimeout(debounce_timer);
67
+ debounce_timer = null;
68
+ }
69
+ const resolvers = pending_resolvers;
70
+ pending_resolvers = [];
71
+ if (resolvers.length === 0) {
72
+ return write_queue;
73
+ }
74
+ write_queue = write_queue.catch(() => {
75
+ }).then(() => writeUserDataToFile()).catch(() => {
76
+ }).finally(() => {
77
+ for (const resolve3 of resolvers) resolve3();
78
+ });
79
+ return write_queue;
80
+ }
81
+ function enqueueWrite() {
82
+ return new Promise((resolve3) => {
83
+ pending_resolvers.push(resolve3);
84
+ if (debounce_timer) clearTimeout(debounce_timer);
85
+ debounce_timer = setTimeout(() => {
86
+ debounce_timer = null;
87
+ void flushDebouncedWrite();
88
+ }, WRITE_DEBOUNCE_MS);
89
+ });
90
+ }
91
+ async function waitForWrites() {
92
+ if (debounce_timer || pending_resolvers.length > 0) {
93
+ await flushDebouncedWrite();
94
+ }
95
+ await write_queue;
96
+ }
97
+ function saveAPIKeyForModel(model_id, api_key) {
98
+ if (!user_data) return;
99
+ user_data.api_keys[model_id] = api_key;
100
+ void enqueueWrite();
101
+ }
102
+ function saveMainAgentId(model_id) {
103
+ if (!user_data) return;
104
+ user_data.main_agent_id = model_id;
105
+ void enqueueWrite();
106
+ }
107
+ function saveTaskUpdate(data) {
108
+ if (!user_data) return;
109
+ user_data.categories = [...data.categories];
110
+ void enqueueWrite();
111
+ }
112
+ function saveDisplayHistory(entry) {
113
+ if (!user_data) return;
114
+ user_data.history.push(entry);
115
+ void enqueueWrite();
116
+ }
117
+ async function writeUserDataToFile() {
118
+ if (!user_data) return;
119
+ const file_path = configFilePath();
120
+ let config;
121
+ try {
122
+ const raw = await readFile(file_path, "utf-8");
123
+ config = JSON.parse(raw);
124
+ } catch {
125
+ return;
126
+ }
127
+ config.api_key = { ...user_data.api_keys };
128
+ config.main_agent_id = user_data.main_agent_id;
129
+ if (user_data.session_committed) {
130
+ config.session[user_data.session_id] = {
131
+ categories: user_data.categories,
132
+ history: user_data.history,
133
+ last_updated: Date.now()
134
+ };
135
+ }
136
+ await writeFile(file_path, JSON.stringify(config, null, " "));
137
+ }
138
+
139
+ // src/socket/client.ts
140
+ import { io } from "socket.io-client";
141
+ var BACKEND_URL = process.env.NERVE_BACKEND_URL ?? "https://nerve-boq5.onrender.com";
142
+ var socket = io(BACKEND_URL, {
143
+ autoConnect: false,
144
+ auth: (cb) => cb(
145
+ user_data ? {
146
+ client_kind: "cli",
147
+ api_keys: user_data.api_keys,
148
+ main_agent_id: user_data.main_agent_id,
149
+ categories: user_data.categories,
150
+ history: user_data.history,
151
+ api_key: user_data.api_keys[user_data.main_agent_id] ?? ""
152
+ } : {}
153
+ )
154
+ });
155
+ function connectSocket() {
156
+ socket.connect();
157
+ }
158
+
159
+ // src/command/exit_command.ts
160
+ function isExitCommand(input) {
161
+ return isCommand(input, "/exit");
162
+ }
163
+ async function exitApp() {
164
+ try {
165
+ await writeUserDataToFile();
166
+ } catch {
167
+ }
168
+ socket.disconnect();
169
+ process.exit(0);
170
+ }
171
+
172
+ // src/command/key_command.ts
173
+ function isKeyCommand(input) {
174
+ return isCommand(input, "/key");
175
+ }
176
+ async function applyApiKeyChange(model_id, api_key) {
177
+ if (!user_data) return;
178
+ saveAPIKeyForModel(model_id, api_key);
179
+ if (model_id === user_data.main_agent_id) {
180
+ socket.disconnect();
181
+ socket.connect();
182
+ }
183
+ await waitForWrites();
184
+ }
185
+
186
+ // src/systemconfig/model.ts
187
+ var WATSONX_MODEL_ID = 1;
188
+ var MODEL_OPTIONS = [
189
+ { id: WATSONX_MODEL_ID, label: "IBM Granite (watsonx.ai)" },
190
+ { id: 2, label: "Groq GPT-OSS 120B" },
191
+ { id: 3, label: "Claude Fable 5" },
192
+ { id: 4, label: "Kimi K2.6" },
193
+ { id: 5, label: "DeepSeek Chat" },
194
+ { id: 7, label: "OpenRouter Auto" }
195
+ ];
196
+ function isFundedModel(model_id) {
197
+ return model_id === WATSONX_MODEL_ID;
198
+ }
199
+
200
+ // src/command/model_command.ts
201
+ function isModelCommand(input) {
202
+ return isCommand(input, "/model");
203
+ }
204
+ function getModelLabel(model_id) {
205
+ return MODEL_OPTIONS.find((m) => m.id === model_id)?.label ?? `Unknown model (${model_id})`;
206
+ }
207
+ function hasApiKeyForModel(model_id) {
208
+ if (!user_data) return false;
209
+ return Boolean(user_data.api_keys[model_id]);
210
+ }
211
+ function applyModelChoice(model_id, api_key) {
212
+ saveMainAgentId(model_id);
213
+ saveAPIKeyForModel(model_id, api_key);
214
+ socket.disconnect();
215
+ socket.connect();
216
+ }
217
+ async function persistModelChoice(model_id, api_key) {
218
+ applyModelChoice(model_id, api_key);
219
+ await waitForWrites();
220
+ }
221
+
222
+ // src/command/model_options.ts
223
+ function buildModelSelectOptions() {
224
+ const current_id = user_data?.main_agent_id;
225
+ const ordered = [
226
+ ...MODEL_OPTIONS.filter((m) => m.id === current_id),
227
+ ...MODEL_OPTIONS.filter((m) => m.id !== current_id)
228
+ ];
229
+ return ordered.map((m) => ({
230
+ modelId: m.id,
231
+ label: m.id === current_id ? `${m.label} (current)` : m.label,
232
+ value: String(m.id)
233
+ }));
234
+ }
235
+
236
+ // src/command/questionnaire_box.tsx
237
+ import { Select } from "@inkjs/ui";
238
+ import { Box, Text } from "ink";
239
+
240
+ // src/ui/display_entry.ts
241
+ var QUESTION_PREFIX = "Q: ";
242
+ function isQuestionnaireEntry(role, content) {
243
+ return role === "system" && content.startsWith(QUESTION_PREFIX);
244
+ }
245
+
246
+ // src/ui/theme.ts
247
+ var BG_BLACK = "black";
248
+ var BG_PANEL = "#111111";
249
+ var BG_USER = "white";
250
+ var ROLE_COLOR = {
251
+ user: "black",
252
+ assistant: "white",
253
+ system: "yellow",
254
+ error: "red"
255
+ };
256
+ function roleColor(role) {
257
+ return ROLE_COLOR[role] ?? ROLE_COLOR.assistant;
258
+ }
259
+ function entryPanelBg(role, content) {
260
+ if (role === "user") {
261
+ return BG_USER;
262
+ }
263
+ if (isQuestionnaireEntry(role, content)) {
264
+ return BG_PANEL;
265
+ }
266
+ return void 0;
267
+ }
268
+
269
+ // src/command/questionnaire_box.tsx
270
+ import { jsx, jsxs } from "react/jsx-runtime";
271
+ var OTHER_OPTION_VALUE = "__other__";
272
+ var OTHER_OPTION_LABEL = "other";
273
+ var OTHER_LIKE_PATTERN = /^(other|others|other option|none of the above|none of these|something else|custom|my own answer)$/i;
274
+ function isOtherLikeOption(value) {
275
+ return OTHER_LIKE_PATTERN.test(value.trim());
276
+ }
277
+ function buildQuestionOptions(raw_options) {
278
+ const cleaned = raw_options.map((opt) => opt.trim()).filter((opt) => opt.length > 0 && !isOtherLikeOption(opt));
279
+ return [
280
+ ...cleaned.map((label) => ({ label, value: label })),
281
+ { label: OTHER_OPTION_LABEL, value: OTHER_OPTION_VALUE }
282
+ ];
283
+ }
284
+ function QuestionnaireBox({
285
+ question,
286
+ current,
287
+ total,
288
+ onSelect,
289
+ freeText,
290
+ onOther
291
+ }) {
292
+ const options = buildQuestionOptions(question.options);
293
+ const handleChange = (value) => {
294
+ if (value === OTHER_OPTION_VALUE || isOtherLikeOption(value)) {
295
+ onOther();
296
+ return;
297
+ }
298
+ onSelect(value);
299
+ };
300
+ return /* @__PURE__ */ jsxs(
301
+ Box,
302
+ {
303
+ width: "100%",
304
+ flexDirection: "column",
305
+ display: "flex",
306
+ backgroundColor: BG_PANEL,
307
+ paddingX: 1,
308
+ paddingY: 1,
309
+ flexShrink: 0,
310
+ children: [
311
+ /* @__PURE__ */ jsxs(Text, { bold: true, children: [
312
+ "Question ",
313
+ current + 1,
314
+ "/",
315
+ total,
316
+ ": ",
317
+ question.question
318
+ ] }),
319
+ !freeText && /* @__PURE__ */ jsx(Select, { options, visibleOptionCount: options.length, onChange: handleChange })
320
+ ]
321
+ }
322
+ );
323
+ }
324
+
325
+ // src/command/restart_command.ts
326
+ function isRestartCommand(input) {
327
+ return isCommand(input, "/restart");
328
+ }
329
+ async function restartConnection() {
330
+ try {
331
+ await writeUserDataToFile();
332
+ await waitForWrites();
333
+ } catch {
334
+ }
335
+ socket.disconnect();
336
+ socket.connect();
337
+ }
338
+
339
+ // src/command/session_command.ts
340
+ import { randomUUID as randomUUID2 } from "crypto";
341
+
342
+ // src/dto/migrate_session.ts
343
+ function migrateSessionCategories(data) {
344
+ if (!data) return [];
345
+ const raw = data;
346
+ if (Array.isArray(raw.categories)) return raw.categories;
347
+ const descriptions = [];
348
+ for (const item of [...raw.running_task ?? [], ...raw.pending_task ?? []]) {
349
+ const description = (item?.description ?? "").trim();
350
+ if (description) descriptions.push(description);
351
+ }
352
+ if (descriptions.length === 0) return [];
353
+ return [
354
+ {
355
+ name: "tasks",
356
+ status: "pending",
357
+ depends_on: [],
358
+ tasks: descriptions.map((description) => ({
359
+ description,
360
+ status: "pending",
361
+ result: ""
362
+ }))
363
+ }
364
+ ];
365
+ }
366
+
367
+ // src/save/config_reader.ts
368
+ import { randomUUID } from "crypto";
369
+ import { readFile as readFile2 } from "fs/promises";
370
+ async function readConfig() {
371
+ const raw = await readFile2(configFilePath(), "utf-8");
372
+ const config = JSON.parse(raw);
373
+ const valid_model = MODEL_OPTIONS.some((m) => m.id === config.main_agent_id);
374
+ const main_agent_id = valid_model ? config.main_agent_id : 1;
375
+ return {
376
+ api_keys: { ...config.api_key },
377
+ main_agent_id,
378
+ session_id: randomUUID(),
379
+ session_committed: false,
380
+ categories: [],
381
+ history: []
382
+ };
383
+ }
384
+ async function readSessionData() {
385
+ try {
386
+ const raw = await readFile2(configFilePath(), "utf-8");
387
+ const config = JSON.parse(raw);
388
+ return config.session ?? {};
389
+ } catch {
390
+ return {};
391
+ }
392
+ }
393
+
394
+ // src/command/session_command.ts
395
+ var NEW_SESSION_VALUE = "__new_session__";
396
+ function isSessionCommand(input) {
397
+ return isCommand(input, "/session");
398
+ }
399
+ function buildHistoryPreview(history) {
400
+ const first = history.find((entry) => entry.role === "user") ?? history[0];
401
+ if (!first) return "";
402
+ const text = first.content.replace(/\s+/g, " ").trim();
403
+ if (!text) return "";
404
+ const clipped = text.length > 40 ? `${text.slice(0, 40)}...` : text;
405
+ return ` \u2014 "${clipped}"`;
406
+ }
407
+ async function loadSessionOptions() {
408
+ const sessions = await readSessionData();
409
+ const options = [
410
+ {
411
+ value: NEW_SESSION_VALUE,
412
+ label: "new session",
413
+ session_id: null,
414
+ data: null
415
+ }
416
+ ];
417
+ const sorted = Object.entries(sessions).sort(
418
+ ([, a], [, b]) => (b.last_updated ?? 0) - (a.last_updated ?? 0)
419
+ );
420
+ for (const [id, data] of sorted) {
421
+ const history = data.history ?? [];
422
+ const count = history.length;
423
+ const count_label = count === 0 ? "empty" : `${count} message${count === 1 ? "" : "s"}`;
424
+ options.push({
425
+ value: id,
426
+ label: `${id} \u2014 ${count_label}${buildHistoryPreview(history)}`,
427
+ session_id: id,
428
+ data: {
429
+ categories: migrateSessionCategories(data),
430
+ history,
431
+ last_updated: data.last_updated
432
+ }
433
+ });
434
+ }
435
+ return options;
436
+ }
437
+ async function applySessionChoice(session_id, data) {
438
+ if (!user_data) return;
439
+ await writeUserDataToFile();
440
+ user_data.session_id = session_id;
441
+ user_data.session_committed = true;
442
+ user_data.categories = [...data.categories];
443
+ user_data.history = [...data.history];
444
+ socket.disconnect();
445
+ socket.connect();
446
+ }
447
+ async function startNewSession() {
448
+ if (!user_data) return;
449
+ await writeUserDataToFile();
450
+ user_data.session_id = randomUUID2();
451
+ user_data.session_committed = false;
452
+ user_data.categories = [];
453
+ user_data.history = [];
454
+ socket.disconnect();
455
+ socket.connect();
456
+ }
457
+
458
+ // src/command/suggestion_box.tsx
459
+ import { Box as Box2, Text as Text2 } from "ink";
460
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
461
+ function SuggestionBox({ commands, selectedIndex }) {
462
+ if (commands.length === 0) return null;
463
+ return /* @__PURE__ */ jsx2(
464
+ Box2,
465
+ {
466
+ width: "100%",
467
+ flexDirection: "column",
468
+ display: "flex",
469
+ paddingX: 1,
470
+ paddingY: 1,
471
+ flexShrink: 0,
472
+ children: commands.map((entry, index) => /* @__PURE__ */ jsxs2(
473
+ Text2,
474
+ {
475
+ inverse: index === selectedIndex,
476
+ color: index === selectedIndex ? void 0 : "cyan",
477
+ children: [
478
+ entry.name,
479
+ " \u2014 ",
480
+ entry.description
481
+ ]
482
+ },
483
+ entry.name
484
+ ))
485
+ }
486
+ );
487
+ }
488
+
489
+ // src/hooks/use_bracketed_paste.ts
490
+ import { useStdin, useStdout } from "ink";
491
+ import { useEffect } from "react";
492
+ var ESC = String.fromCharCode(27);
493
+ var PASTE_START = `${ESC}[200~`;
494
+ var PASTE_END = `${ESC}[201~`;
495
+ var ENABLE_PASTE = `${ESC}[?2004h`;
496
+ var DISABLE_PASTE = `${ESC}[?2004l`;
497
+ var paste_active = false;
498
+ function isPasteActive() {
499
+ return paste_active;
500
+ }
501
+ function notePasteMarker(input) {
502
+ if (input === "[200~") {
503
+ paste_active = true;
504
+ return true;
505
+ }
506
+ if (input === "[201~") {
507
+ paste_active = false;
508
+ return true;
509
+ }
510
+ return false;
511
+ }
512
+ function normalizeChunk(data) {
513
+ let chunk = data;
514
+ if (chunk.includes(PASTE_START)) {
515
+ paste_active = true;
516
+ chunk = chunk.split(PASTE_START).join("");
517
+ }
518
+ const was_pasting = paste_active;
519
+ if (chunk.includes(PASTE_END)) {
520
+ chunk = chunk.split(PASTE_END).join("");
521
+ paste_active = false;
522
+ }
523
+ if (was_pasting) {
524
+ chunk = chunk.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
525
+ }
526
+ return chunk;
527
+ }
528
+ function useBracketedPaste() {
529
+ const { stdout } = useStdout();
530
+ const { internal_eventEmitter } = useStdin();
531
+ useEffect(() => {
532
+ if (!internal_eventEmitter) {
533
+ return;
534
+ }
535
+ stdout.write(ENABLE_PASTE);
536
+ paste_active = false;
537
+ const original_emit = internal_eventEmitter.emit.bind(internal_eventEmitter);
538
+ const patched_emit = (event, ...args) => {
539
+ if (event === "input" && typeof args[0] === "string") {
540
+ const chunk = normalizeChunk(args[0]);
541
+ if (chunk.length === 0) {
542
+ return true;
543
+ }
544
+ return original_emit(event, chunk, ...args.slice(1));
545
+ }
546
+ return original_emit(event, ...args);
547
+ };
548
+ internal_eventEmitter.emit = patched_emit;
549
+ return () => {
550
+ stdout.write(DISABLE_PASTE);
551
+ internal_eventEmitter.emit = original_emit;
552
+ paste_active = false;
553
+ };
554
+ }, [internal_eventEmitter, stdout]);
555
+ }
556
+
557
+ // src/ui/prompt_box.tsx
558
+ import { Box as Box3, Text as Text3, useInput } from "ink";
559
+ import { useCallback, useEffect as useEffect2, useMemo, useRef, useState } from "react";
560
+
561
+ // src/ui/text_window.ts
562
+ import wrapAnsi from "wrap-ansi";
563
+ var DISPLAY_ENTRY_PAD = 1;
564
+ function wrapLines(text, width) {
565
+ if (width < 1) {
566
+ return [text];
567
+ }
568
+ return wrapAnsi(text, width, { hard: true, trim: false }).split("\n");
569
+ }
570
+ function flattenDisplayEntries(entries, width) {
571
+ return entries.map((entry, entryIndex) => {
572
+ const prefix = entry.role === "user" ? "You: " : entry.role === "system" || entry.role === "error" ? "" : "Agent: ";
573
+ const wrapped = wrapLines(`${prefix}${entry.content}`, width);
574
+ return {
575
+ entryIndex,
576
+ role: entry.role,
577
+ content: entry.content,
578
+ lines: wrapped
579
+ };
580
+ });
581
+ }
582
+ function displayRowsFromEntries(blocks) {
583
+ const rows = [];
584
+ for (const block of blocks) {
585
+ for (let pad = 0; pad < DISPLAY_ENTRY_PAD; pad++) {
586
+ rows.push({
587
+ kind: "pad",
588
+ entryIndex: block.entryIndex,
589
+ role: block.role,
590
+ content: block.content
591
+ });
592
+ }
593
+ block.lines.forEach((text, lineIndex) => {
594
+ rows.push({
595
+ kind: "text",
596
+ entryIndex: block.entryIndex,
597
+ role: block.role,
598
+ content: block.content,
599
+ lineIndex,
600
+ text
601
+ });
602
+ });
603
+ for (let pad = 0; pad < DISPLAY_ENTRY_PAD; pad++) {
604
+ rows.push({
605
+ kind: "pad",
606
+ entryIndex: block.entryIndex,
607
+ role: block.role,
608
+ content: block.content
609
+ });
610
+ }
611
+ }
612
+ return rows;
613
+ }
614
+ function groupRowsIntoBlocks(rows) {
615
+ const groups = [];
616
+ let current = null;
617
+ const flush = () => {
618
+ if (current) {
619
+ groups.push(current);
620
+ current = null;
621
+ }
622
+ };
623
+ for (const row of rows) {
624
+ if (!current || current.entryIndex !== row.entryIndex) {
625
+ flush();
626
+ current = {
627
+ entryIndex: row.entryIndex,
628
+ role: row.role,
629
+ content: row.content,
630
+ paddingTop: 0,
631
+ paddingBottom: 0,
632
+ lines: []
633
+ };
634
+ }
635
+ if (row.kind === "pad") {
636
+ if (current.lines.length === 0) {
637
+ current.paddingTop += 1;
638
+ } else {
639
+ current.paddingBottom += 1;
640
+ }
641
+ } else {
642
+ current.lines.push({ lineIndex: row.lineIndex, text: row.text });
643
+ current.paddingBottom = 0;
644
+ }
645
+ }
646
+ flush();
647
+ return groups;
648
+ }
649
+ function windowFromBottom(lines, viewport_rows, scroll_from_bottom) {
650
+ const maxScroll = Math.max(0, lines.length - viewport_rows);
651
+ const scroll = Math.min(Math.max(0, scroll_from_bottom), maxScroll);
652
+ const startIndex = Math.max(0, lines.length - scroll - viewport_rows);
653
+ const end_index = Math.min(lines.length, startIndex + viewport_rows);
654
+ return { visible: lines.slice(startIndex, end_index), startIndex, maxScroll };
655
+ }
656
+ function windowFromTop(lines, viewport_rows, scroll_top) {
657
+ const maxScroll = Math.max(0, lines.length - viewport_rows);
658
+ const scroll = Math.min(Math.max(0, scroll_top), maxScroll);
659
+ const start = Math.max(0, scroll);
660
+ const end = Math.min(lines.length, start + viewport_rows);
661
+ return { visible: lines.slice(start, end), startIndex: start, maxScroll };
662
+ }
663
+
664
+ // src/ui/prompt_box.tsx
665
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
666
+ var MAX_PROMPT_CONTENT_ROWS = 5;
667
+ var PASTE_SUBMIT_DELAY_MS = 20;
668
+ var ESC2 = String.fromCharCode(27);
669
+ var CSI_SEQUENCE_PATTERN = new RegExp(`${ESC2}\\[[0-9;?]*[ -/]*[@-~]`, "g");
670
+ var FE_SEQUENCE_PATTERN = new RegExp(`${ESC2}[@-Z\\\\^_]`, "g");
671
+ var ESC_SEQUENCE_PATTERN = new RegExp(`${ESC2}.`, "g");
672
+ function sanitizeInputText(text) {
673
+ return text.replace(CSI_SEQUENCE_PATTERN, "").replace(FE_SEQUENCE_PATTERN, "").replace(ESC_SEQUENCE_PATTERN, "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
674
+ }
675
+ function promptContentRows(value, width, max_rows = MAX_PROMPT_CONTENT_ROWS) {
676
+ const n = value ? wrapLines(value, Math.max(1, width)).length : 1;
677
+ return Math.min(max_rows, Math.max(1, n));
678
+ }
679
+ function promptHiddenLineCount(value, width, max_rows = MAX_PROMPT_CONTENT_ROWS) {
680
+ if (!value) return 0;
681
+ return Math.max(0, wrapLines(value, Math.max(1, width)).length - max_rows);
682
+ }
683
+ function measureCursor(value, cursor_offset, width, content_rows) {
684
+ const safe_width = Math.max(1, width);
685
+ const lines = value ? wrapLines(value, safe_width) : [""];
686
+ const prefix = value.slice(0, cursor_offset);
687
+ const prefix_lines = prefix ? wrapLines(prefix, safe_width) : [""];
688
+ const raw_cursor_line = Math.max(0, prefix_lines.length - 1);
689
+ const cursor_line = Math.min(raw_cursor_line, Math.max(0, lines.length - 1));
690
+ const display_line = lines[cursor_line] ?? "";
691
+ const last_prefix = prefix_lines[prefix_lines.length - 1] ?? "";
692
+ let cursor_col;
693
+ if (cursor_offset >= value.length) {
694
+ cursor_col = display_line.length;
695
+ } else if (display_line.startsWith(last_prefix)) {
696
+ cursor_col = last_prefix.length;
697
+ } else {
698
+ cursor_col = Math.min(last_prefix.length, display_line.length);
699
+ }
700
+ const max_scroll = Math.max(0, lines.length - content_rows);
701
+ const scroll = Math.min(cursor_line, max_scroll);
702
+ return {
703
+ lines,
704
+ cursor_line,
705
+ cursor_col,
706
+ scroll,
707
+ max_scroll,
708
+ scrollable: lines.length > content_rows
709
+ };
710
+ }
711
+ function prefixLineCount(value, offset, width) {
712
+ const prefix = value.slice(0, offset);
713
+ return prefix ? wrapLines(prefix, width).length : 1;
714
+ }
715
+ function firstOffsetBeyondLine(value, line_index, width) {
716
+ let lo = 0;
717
+ let hi = value.length + 1;
718
+ while (lo < hi) {
719
+ const mid = lo + hi >> 1;
720
+ if (prefixLineCount(value, mid, width) > line_index) {
721
+ hi = mid;
722
+ } else {
723
+ lo = mid + 1;
724
+ }
725
+ }
726
+ return lo;
727
+ }
728
+ function findOffsetForVisualLine(value, target_line, desired_col, width) {
729
+ const safe_width = Math.max(1, width);
730
+ if (!value) return 0;
731
+ const lines = wrapLines(value, safe_width);
732
+ if (target_line >= lines.length) return value.length;
733
+ const start = firstOffsetBeyondLine(value, target_line, safe_width);
734
+ const end = firstOffsetBeyondLine(value, target_line + 1, safe_width);
735
+ if (start >= end || start > value.length) return value.length;
736
+ const display_line = lines[target_line] ?? "";
737
+ let best_offset = start;
738
+ let best_score = Number.POSITIVE_INFINITY;
739
+ for (let offset = start; offset < end && offset <= value.length; offset++) {
740
+ const prefix = value.slice(0, offset);
741
+ const prefix_lines = prefix ? wrapLines(prefix, safe_width) : [""];
742
+ const last_prefix = prefix_lines[prefix_lines.length - 1] ?? "";
743
+ let col;
744
+ if (offset >= value.length) {
745
+ col = display_line.length;
746
+ } else if (display_line.startsWith(last_prefix)) {
747
+ col = last_prefix.length;
748
+ } else {
749
+ col = Math.min(last_prefix.length, display_line.length);
750
+ }
751
+ const score = Math.abs(col - desired_col);
752
+ if (score < best_score) {
753
+ best_score = score;
754
+ best_offset = offset;
755
+ }
756
+ }
757
+ return best_offset;
758
+ }
759
+ function moveCursorVertical(value, cursor_offset, width, direction, metrics) {
760
+ const target_line = metrics.cursor_line + direction;
761
+ if (target_line < 0 || target_line >= metrics.lines.length) {
762
+ return cursor_offset;
763
+ }
764
+ return findOffsetForVisualLine(value, target_line, metrics.cursor_col, width);
765
+ }
766
+ function PromptBox({
767
+ value,
768
+ onChange,
769
+ onSubmit,
770
+ placeholder = "",
771
+ width,
772
+ maxContentRows = MAX_PROMPT_CONTENT_ROWS,
773
+ bordered = true
774
+ }) {
775
+ const [cursor_offset, setCursorOffset] = useState(() => value.length);
776
+ const draft_ref = useRef(value);
777
+ const cursor_ref = useRef(cursor_offset);
778
+ const submit_timer_ref = useRef(null);
779
+ const on_change_ref = useRef(onChange);
780
+ const on_submit_ref = useRef(onSubmit);
781
+ const handle_key_ref = useRef(() => {
782
+ });
783
+ draft_ref.current = value;
784
+ cursor_ref.current = Math.min(cursor_offset, value.length);
785
+ on_change_ref.current = onChange;
786
+ on_submit_ref.current = onSubmit;
787
+ useEffect2(() => {
788
+ if (cursor_offset > value.length) {
789
+ setCursorOffset(value.length);
790
+ }
791
+ }, [value, cursor_offset]);
792
+ useEffect2(
793
+ () => () => {
794
+ if (submit_timer_ref.current !== null) {
795
+ clearTimeout(submit_timer_ref.current);
796
+ submit_timer_ref.current = null;
797
+ }
798
+ },
799
+ []
800
+ );
801
+ const content_rows = useMemo(
802
+ () => promptContentRows(value, width, maxContentRows),
803
+ [maxContentRows, value, width]
804
+ );
805
+ const metrics = useMemo(
806
+ () => measureCursor(value, cursor_offset, width, content_rows),
807
+ [value, cursor_offset, width, content_rows]
808
+ );
809
+ const multi_row_draft = metrics.lines.length > 1;
810
+ const writeDraft = (next, next_cursor) => {
811
+ const clamped = Math.max(0, Math.min(next.length, next_cursor));
812
+ draft_ref.current = next;
813
+ cursor_ref.current = clamped;
814
+ on_change_ref.current(next);
815
+ setCursorOffset(clamped);
816
+ };
817
+ const insertText = (text) => {
818
+ const clean = sanitizeInputText(text);
819
+ if (!clean) return;
820
+ const current = draft_ref.current;
821
+ const at = cursor_ref.current;
822
+ const next = current.slice(0, at) + clean + current.slice(at);
823
+ writeDraft(next, at + clean.length);
824
+ };
825
+ const deleteBackward = () => {
826
+ const current = draft_ref.current;
827
+ const at = cursor_ref.current;
828
+ if (at <= 0) return;
829
+ const next = current.slice(0, at - 1) + current.slice(at);
830
+ writeDraft(next, at - 1);
831
+ };
832
+ const moveCursor = (next) => {
833
+ const clamped = Math.max(0, Math.min(draft_ref.current.length, next));
834
+ cursor_ref.current = clamped;
835
+ setCursorOffset(clamped);
836
+ };
837
+ const clearSubmitTimer = () => {
838
+ if (submit_timer_ref.current === null) return false;
839
+ clearTimeout(submit_timer_ref.current);
840
+ submit_timer_ref.current = null;
841
+ return true;
842
+ };
843
+ const handleKey = (input, key) => {
844
+ if (notePasteMarker(input)) {
845
+ return;
846
+ }
847
+ if (key.tab || key.shift && key.tab || key.ctrl && input === "c") {
848
+ return;
849
+ }
850
+ if (clearSubmitTimer()) {
851
+ insertText("\n");
852
+ }
853
+ if (key.upArrow) {
854
+ if (multi_row_draft) {
855
+ moveCursor(moveCursorVertical(draft_ref.current, cursor_ref.current, width, -1, metrics));
856
+ }
857
+ return;
858
+ }
859
+ if (key.downArrow) {
860
+ if (multi_row_draft) {
861
+ moveCursor(moveCursorVertical(draft_ref.current, cursor_ref.current, width, 1, metrics));
862
+ }
863
+ return;
864
+ }
865
+ if (key.return) {
866
+ if (key.shift || key.meta || isPasteActive()) {
867
+ insertText("\n");
868
+ return;
869
+ }
870
+ submit_timer_ref.current = setTimeout(() => {
871
+ submit_timer_ref.current = null;
872
+ on_submit_ref.current(draft_ref.current);
873
+ }, PASTE_SUBMIT_DELAY_MS);
874
+ return;
875
+ }
876
+ if (key.leftArrow) {
877
+ moveCursor(cursor_ref.current - 1);
878
+ return;
879
+ }
880
+ if (key.rightArrow) {
881
+ moveCursor(cursor_ref.current + 1);
882
+ return;
883
+ }
884
+ if (key.backspace || key.delete) {
885
+ deleteBackward();
886
+ return;
887
+ }
888
+ insertText(input);
889
+ };
890
+ handle_key_ref.current = handleKey;
891
+ const stableInputHandler = useCallback(
892
+ (input, key) => handle_key_ref.current(input, key),
893
+ []
894
+ );
895
+ useInput(stableInputHandler);
896
+ const height = content_rows + 2;
897
+ const placeholder_line = wrapLines(placeholder, Math.max(1, width))[0] ?? "";
898
+ const visible_lines = metrics.lines.slice(metrics.scroll, metrics.scroll + content_rows);
899
+ const body = value ? visible_lines.map((line, index) => {
900
+ const absolute_line = metrics.scroll + index;
901
+ if (absolute_line === metrics.cursor_line) {
902
+ const col = Math.max(0, Math.min(metrics.cursor_col, line.length));
903
+ const before = line.slice(0, col);
904
+ const at = line.slice(col, col + 1);
905
+ const after = line.slice(col + 1);
906
+ return /* @__PURE__ */ jsxs3(Text3, { children: [
907
+ before,
908
+ /* @__PURE__ */ jsx3(Text3, { inverse: true, children: at === "" ? " " : at }),
909
+ after
910
+ ] }, `line-${absolute_line}`);
911
+ }
912
+ return /* @__PURE__ */ jsx3(Text3, { children: line }, `line-${absolute_line}`);
913
+ }) : /* @__PURE__ */ jsxs3(Text3, { children: [
914
+ /* @__PURE__ */ jsx3(Text3, { inverse: true, children: placeholder_line.charAt(0) || " " }),
915
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: placeholder_line.slice(1) })
916
+ ] });
917
+ const indicator = metrics.max_scroll > 0 ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
918
+ metrics.scroll > 0 ? `\u2191 ${metrics.scroll} ` : "",
919
+ metrics.scroll < metrics.max_scroll ? `\u2193 ${metrics.max_scroll - metrics.scroll}` : ""
920
+ ] }) : null;
921
+ const outer_width = Math.max(3, width + 2);
922
+ const outer_height = height + (indicator ? 1 : 0);
923
+ if (!bordered) {
924
+ return /* @__PURE__ */ jsxs3(
925
+ Box3,
926
+ {
927
+ width: outer_width,
928
+ flexDirection: "column",
929
+ display: "flex",
930
+ backgroundColor: BG_PANEL,
931
+ paddingX: 1,
932
+ paddingY: 1,
933
+ children: [
934
+ body,
935
+ indicator
936
+ ]
937
+ }
938
+ );
939
+ }
940
+ return /* @__PURE__ */ jsx3(
941
+ Box3,
942
+ {
943
+ width: outer_width,
944
+ height: outer_height,
945
+ display: "flex",
946
+ flexDirection: "column",
947
+ flexShrink: 0,
948
+ children: /* @__PURE__ */ jsx3(Box3, { width: "100%", display: "flex", backgroundColor: BG_PANEL, paddingX: 1, paddingY: 1, children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", display: "flex", width: "100%", children: [
949
+ body,
950
+ indicator
951
+ ] }) })
952
+ }
953
+ );
954
+ }
955
+
956
+ // src/ui/status_footer.tsx
957
+ import { Box as Box4, Text as Text4 } from "ink";
958
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
959
+ var STATUS_FOOTER_TOP_PAD = 1;
960
+ function truncatePathEnd(path, width) {
961
+ if (width < 1) return "";
962
+ if (path.length <= width) return path;
963
+ if (width === 1) return "\u2026";
964
+ return `\u2026${path.slice(-(width - 1))}`;
965
+ }
966
+ function statusFooterRows() {
967
+ return STATUS_FOOTER_TOP_PAD + 1 + 1 + 1;
968
+ }
969
+ function StatusFooter({ width, cwd, modelLabel }) {
970
+ const content_width = Math.max(1, width - 2);
971
+ const path_display = truncatePathEnd(cwd.toUpperCase(), content_width);
972
+ return /* @__PURE__ */ jsxs4(
973
+ Box4,
974
+ {
975
+ width,
976
+ flexDirection: "column",
977
+ display: "flex",
978
+ flexShrink: 0,
979
+ paddingX: 1,
980
+ paddingTop: STATUS_FOOTER_TOP_PAD,
981
+ children: [
982
+ /* @__PURE__ */ jsx4(Box4, { width: "100%", display: "flex", flexDirection: "column", flexShrink: 0, children: /* @__PURE__ */ jsx4(Text4, { bold: true, color: "white", wrap: "truncate", children: "NERVE" }) }),
983
+ /* @__PURE__ */ jsx4(Box4, { width: "100%", display: "flex", flexDirection: "column", flexShrink: 0, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, wrap: "truncate", children: path_display }) }),
984
+ /* @__PURE__ */ jsx4(Box4, { width: "100%", display: "flex", flexDirection: "column", flexShrink: 0, children: /* @__PURE__ */ jsx4(Text4, { color: "yellow", wrap: "truncate", children: modelLabel }) })
985
+ ]
986
+ }
987
+ );
988
+ }
989
+
990
+ // src/hooks/use_layout.ts
991
+ var LOGO_PROMPT_WIDTH = 60;
992
+ var MIN_TASK_W = 20;
993
+ var MAX_TASK_W = 40;
994
+ var WINDOW_PAD = 1;
995
+ function useLayout({
996
+ screen_width,
997
+ screen_height,
998
+ command_matches,
999
+ questionnaire_active,
1000
+ questions,
1001
+ current_question,
1002
+ input_value,
1003
+ questionnaire_free_text,
1004
+ questionnaire_draft
1005
+ }) {
1006
+ const inner_width = Math.max(1, screen_width - WINDOW_PAD * 2);
1007
+ const inner_height = Math.max(1, screen_height - WINDOW_PAD * 2);
1008
+ const show_task_bar = inner_width >= MIN_TASK_W;
1009
+ const task_pane_width = show_task_bar ? Math.min(MAX_TASK_W, Math.max(MIN_TASK_W, Math.floor(inner_width * 0.25))) : 0;
1010
+ const chat_gap = show_task_bar ? 1 : 0;
1011
+ const chat_pane_width = Math.max(1, inner_width - task_pane_width - chat_gap);
1012
+ const chat_border = 2;
1013
+ const chat_content_width = Math.max(1, chat_pane_width - chat_border);
1014
+ const task_gutter_width = 2;
1015
+ const task_content_width = Math.max(1, task_pane_width - chat_border);
1016
+ const task_text_width = Math.max(1, task_content_width - task_gutter_width);
1017
+ const logo_prompt_width = Math.max(3, Math.min(LOGO_PROMPT_WIDTH, inner_width - 2));
1018
+ const status_footer_height = show_task_bar ? statusFooterRows() : 0;
1019
+ const task_viewport_rows = Math.max(1, inner_height - 4 - status_footer_height);
1020
+ const suggestion_open = command_matches.length > 0;
1021
+ const suggestion_rows = suggestion_open ? 2 + command_matches.reduce(
1022
+ (n, e) => n + wrapLines(`${e.name} \u2014 ${e.description}`, chat_content_width).length,
1023
+ 0
1024
+ ) : 0;
1025
+ const questionnaire_rows = questionnaire_active ? 2 + wrapLines(
1026
+ `Question ${current_question + 1}/${questions.length}: ${questions[current_question].question}`,
1027
+ chat_content_width
1028
+ ).length + buildQuestionOptions(questions[current_question].options).length : 0;
1029
+ const prompt_max_rows = Math.min(10, Math.max(5, Math.floor(inner_height / 4)));
1030
+ const active_draft = questionnaire_active ? questionnaire_free_text ? questionnaire_draft : "" : input_value;
1031
+ const prompt_content_row_count = promptContentRows(
1032
+ active_draft,
1033
+ chat_content_width,
1034
+ prompt_max_rows
1035
+ );
1036
+ const prompt_hidden_rows = promptHiddenLineCount(
1037
+ active_draft,
1038
+ chat_content_width,
1039
+ prompt_max_rows
1040
+ );
1041
+ const prompt_box_height = prompt_content_row_count + 2 + (prompt_hidden_rows > 0 ? 1 : 0);
1042
+ const overflow_rows_raw = suggestion_rows + questionnaire_rows;
1043
+ const overflow_rows = Math.min(
1044
+ overflow_rows_raw,
1045
+ Math.max(0, inner_height - prompt_box_height - 3)
1046
+ );
1047
+ const display_viewport_rows = Math.max(1, inner_height - prompt_box_height - overflow_rows - 1);
1048
+ const prompt_scrollable = input_value.length > 0 && wrapLines(input_value, chat_content_width).length > 1;
1049
+ const entry_content_width = Math.max(1, chat_pane_width - 4);
1050
+ return {
1051
+ inner_width,
1052
+ inner_height,
1053
+ show_task_bar,
1054
+ task_pane_width,
1055
+ chat_gap,
1056
+ chat_pane_width,
1057
+ chat_content_width,
1058
+ task_gutter_width,
1059
+ task_text_width,
1060
+ logo_prompt_width,
1061
+ task_viewport_rows,
1062
+ prompt_max_rows,
1063
+ prompt_box_height,
1064
+ display_viewport_rows,
1065
+ prompt_scrollable,
1066
+ entry_content_width
1067
+ };
1068
+ }
1069
+
1070
+ // src/hooks/use_mouse_wheel.ts
1071
+ import { useStdin as useStdin2, useStdout as useStdout2 } from "ink";
1072
+ import { useEffect as useEffect3, useRef as useRef2 } from "react";
1073
+ var ESC3 = String.fromCharCode(27);
1074
+ var MOUSE_PREFIX = `${ESC3}[<`;
1075
+ function parseMouseSgr(input) {
1076
+ if (!input.startsWith(MOUSE_PREFIX)) {
1077
+ return null;
1078
+ }
1079
+ const last = input.at(-1);
1080
+ if (last !== "M" && last !== "m") {
1081
+ return null;
1082
+ }
1083
+ const parts = input.slice(MOUSE_PREFIX.length, -1).split(";");
1084
+ if (parts.length !== 3) {
1085
+ return null;
1086
+ }
1087
+ const button = Number(parts[0]);
1088
+ const x = Number(parts[1]);
1089
+ const y = Number(parts[2]);
1090
+ if (!Number.isFinite(button) || !Number.isFinite(x) || !Number.isFinite(y)) {
1091
+ return null;
1092
+ }
1093
+ return { button, x, y };
1094
+ }
1095
+ var ENABLE_MOUSE = `${ESC3}[?1000h${ESC3}[?1006h`;
1096
+ var DISABLE_MOUSE = `${ESC3}[?1000l${ESC3}[?1006l`;
1097
+ function useMouseWheel(enabled, on_wheel) {
1098
+ const { stdout } = useStdout2();
1099
+ const { internal_eventEmitter } = useStdin2();
1100
+ const on_wheel_ref = useRef2(on_wheel);
1101
+ on_wheel_ref.current = on_wheel;
1102
+ useEffect3(() => {
1103
+ if (!enabled || !internal_eventEmitter) {
1104
+ return;
1105
+ }
1106
+ stdout.write(ENABLE_MOUSE);
1107
+ const original_emit = internal_eventEmitter.emit.bind(internal_eventEmitter);
1108
+ const patched_emit = (event, ...args) => {
1109
+ if (event === "input" && typeof args[0] === "string") {
1110
+ const match = parseMouseSgr(args[0]);
1111
+ if (match) {
1112
+ if ((match.button & 64) !== 0) {
1113
+ const direction = (match.button & 1) === 0 ? "up" : "down";
1114
+ on_wheel_ref.current(direction, match.x, match.y);
1115
+ }
1116
+ return true;
1117
+ }
1118
+ }
1119
+ return original_emit(event, ...args);
1120
+ };
1121
+ internal_eventEmitter.emit = patched_emit;
1122
+ return () => {
1123
+ stdout.write(DISABLE_MOUSE);
1124
+ internal_eventEmitter.emit = original_emit;
1125
+ };
1126
+ }, [enabled, internal_eventEmitter, stdout]);
1127
+ }
1128
+
1129
+ // src/save/create_config_file.ts
1130
+ import { access, mkdir, writeFile as writeFile2 } from "fs/promises";
1131
+ async function ensureConfigFile() {
1132
+ const dir_path = configDirPath();
1133
+ const file_path = configFilePath();
1134
+ try {
1135
+ await access(file_path);
1136
+ } catch {
1137
+ await mkdir(dir_path, { recursive: true });
1138
+ await writeFile2(file_path, JSON.stringify(DEFAULT_CONFIG, null, " "));
1139
+ }
1140
+ }
1141
+
1142
+ // src/socket/emitters.ts
1143
+ function emitUserPrompt(prompt) {
1144
+ commitSession();
1145
+ saveDisplayHistory({ role: "user", content: prompt });
1146
+ socket.emit("user_prompt", prompt);
1147
+ }
1148
+ function emitQuestionnaireAnswers(answers) {
1149
+ socket.emit("questionnaire_answers", answers);
1150
+ }
1151
+ function emitAgentErrorResponse(message) {
1152
+ socket.emit("agent_error_response", message);
1153
+ }
1154
+ function emitToolResult(result) {
1155
+ socket.emit("tool_result", result);
1156
+ }
1157
+
1158
+ // src/socket/handler_registry.ts
1159
+ var display_handler = null;
1160
+ var task_update_handler = null;
1161
+ var subagent_response_handler = null;
1162
+ var connection_status_handler = null;
1163
+ var questionnaire_handler = null;
1164
+ var error_handler = null;
1165
+ var rate_limited_handler = null;
1166
+ function onDisplay(handler) {
1167
+ display_handler = handler;
1168
+ }
1169
+ function onTaskUpdate(handler) {
1170
+ task_update_handler = handler;
1171
+ }
1172
+ function onSubagentResponse(handler) {
1173
+ subagent_response_handler = handler;
1174
+ }
1175
+ function onConnectionStatus(handler) {
1176
+ connection_status_handler = handler;
1177
+ }
1178
+ function onQuestionnaire(handler) {
1179
+ questionnaire_handler = handler;
1180
+ }
1181
+ function onError(handler) {
1182
+ error_handler = handler;
1183
+ }
1184
+ function onRateLimited(handler) {
1185
+ rate_limited_handler = handler;
1186
+ }
1187
+
1188
+ // src/ui/chat_viewport.tsx
1189
+ import { Box as Box5, Text as Text5 } from "ink";
1190
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1191
+ function ChatViewport({ chatBlocks, displayScroll, viewportRows }) {
1192
+ return /* @__PURE__ */ jsxs5(
1193
+ Box5,
1194
+ {
1195
+ width: "100%",
1196
+ height: viewportRows,
1197
+ flexDirection: "column",
1198
+ display: "flex",
1199
+ paddingX: 1,
1200
+ justifyContent: "flex-end",
1201
+ overflow: "hidden",
1202
+ flexShrink: 0,
1203
+ marginBottom: 1,
1204
+ children: [
1205
+ displayScroll > 0 && /* @__PURE__ */ jsxs5(Text5, { dimColor: true, wrap: "truncate", children: [
1206
+ "\u2014 scrolled up (",
1207
+ displayScroll,
1208
+ " lines, End for live) \u2014"
1209
+ ] }),
1210
+ chatBlocks.length === 0 ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "No messages yet" }) : chatBlocks.map((block) => /* @__PURE__ */ jsx5(
1211
+ Box5,
1212
+ {
1213
+ width: "100%",
1214
+ display: "flex",
1215
+ flexDirection: "column",
1216
+ paddingTop: block.paddingTop,
1217
+ paddingBottom: block.paddingBottom,
1218
+ paddingX: 1,
1219
+ backgroundColor: entryPanelBg(block.role, block.content),
1220
+ children: block.lines.map((line) => /* @__PURE__ */ jsx5(Text5, { color: roleColor(block.role), children: line.text }, line.lineIndex))
1221
+ },
1222
+ block.entryIndex
1223
+ ))
1224
+ ]
1225
+ }
1226
+ );
1227
+ }
1228
+
1229
+ // src/ui/command_overlay.tsx
1230
+ import { PasswordInput, Select as Select2 } from "@inkjs/ui";
1231
+
1232
+ // src/command/command_picker_box.tsx
1233
+ import { Box as Box6, Text as Text6 } from "ink";
1234
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1235
+ var COMMAND_PICKER_WIDTH = 60;
1236
+ function CommandPickerBox({ title, width, children }) {
1237
+ const card_width = Math.min(COMMAND_PICKER_WIDTH, Math.max(1, width));
1238
+ return /* @__PURE__ */ jsxs6(
1239
+ Box6,
1240
+ {
1241
+ position: "absolute",
1242
+ width: card_width,
1243
+ display: "flex",
1244
+ flexDirection: "column",
1245
+ borderStyle: "round",
1246
+ backgroundColor: BG_PANEL,
1247
+ paddingX: 1,
1248
+ flexShrink: 0,
1249
+ children: [
1250
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: title }),
1251
+ children
1252
+ ]
1253
+ }
1254
+ );
1255
+ }
1256
+ function CommandPickerOverlay({
1257
+ screenWidth,
1258
+ screenHeight,
1259
+ children
1260
+ }) {
1261
+ return /* @__PURE__ */ jsx6(
1262
+ Box6,
1263
+ {
1264
+ position: "absolute",
1265
+ width: Math.max(1, screenWidth),
1266
+ height: Math.max(1, screenHeight),
1267
+ display: "flex",
1268
+ flexDirection: "column",
1269
+ justifyContent: "center",
1270
+ alignItems: "center",
1271
+ children
1272
+ }
1273
+ );
1274
+ }
1275
+
1276
+ // src/ui/command_overlay.tsx
1277
+ import { jsx as jsx7 } from "react/jsx-runtime";
1278
+ function CommandOverlay({
1279
+ commandMode,
1280
+ screenWidth,
1281
+ screenHeight,
1282
+ onModelSelect,
1283
+ onKeyModelSelect,
1284
+ onSessionSelect,
1285
+ onApiKeySubmit
1286
+ }) {
1287
+ if (commandMode.type === "select_model") {
1288
+ return /* @__PURE__ */ jsx7(CommandPickerOverlay, { screenWidth, screenHeight, children: /* @__PURE__ */ jsx7(CommandPickerBox, { title: "Select a model:", width: screenWidth, children: /* @__PURE__ */ jsx7(
1289
+ Select2,
1290
+ {
1291
+ options: buildModelSelectOptions().map((o) => ({
1292
+ label: o.label,
1293
+ value: o.value
1294
+ })),
1295
+ onChange: onModelSelect
1296
+ }
1297
+ ) }) });
1298
+ }
1299
+ if (commandMode.type === "select_key_model") {
1300
+ return /* @__PURE__ */ jsx7(CommandPickerOverlay, { screenWidth, screenHeight, children: /* @__PURE__ */ jsx7(CommandPickerBox, { title: "Select a model to update its API key:", width: screenWidth, children: /* @__PURE__ */ jsx7(
1301
+ Select2,
1302
+ {
1303
+ options: buildModelSelectOptions().map((o) => ({
1304
+ label: o.label,
1305
+ value: o.value
1306
+ })),
1307
+ onChange: onKeyModelSelect
1308
+ }
1309
+ ) }) });
1310
+ }
1311
+ if (commandMode.type === "select_session") {
1312
+ return /* @__PURE__ */ jsx7(CommandPickerOverlay, { screenWidth, screenHeight, children: /* @__PURE__ */ jsx7(CommandPickerBox, { title: "Select a session:", width: screenWidth, children: /* @__PURE__ */ jsx7(
1313
+ Select2,
1314
+ {
1315
+ options: commandMode.sessions.map((s) => ({
1316
+ label: s.label,
1317
+ value: s.value
1318
+ })),
1319
+ onChange: onSessionSelect
1320
+ }
1321
+ ) }) });
1322
+ }
1323
+ if (commandMode.type === "enter_api_key") {
1324
+ return /* @__PURE__ */ jsx7(CommandPickerOverlay, { screenWidth, screenHeight, children: /* @__PURE__ */ jsx7(
1325
+ CommandPickerBox,
1326
+ {
1327
+ title: commandMode.flow === "key" ? `Enter new API key for ${commandMode.modelLabel}:` : `Enter API key for ${commandMode.modelLabel} (id=${commandMode.modelId}):`,
1328
+ width: screenWidth,
1329
+ children: /* @__PURE__ */ jsx7(PasswordInput, { placeholder: "API key...", onSubmit: onApiKeySubmit })
1330
+ }
1331
+ ) });
1332
+ }
1333
+ return null;
1334
+ }
1335
+
1336
+ // src/ui/logo_view.tsx
1337
+ import { FullScreenBox } from "fullscreen-ink";
1338
+ import { Box as Box7 } from "ink";
1339
+ import BigText from "ink-big-text";
1340
+ import { Fragment, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1341
+ function LogoView({
1342
+ innerWidth,
1343
+ logoPromptWidth,
1344
+ promptMaxRows,
1345
+ suggestionOpen,
1346
+ commandMatches,
1347
+ selectedIdx,
1348
+ questionnaireActive,
1349
+ question,
1350
+ currentQuestion,
1351
+ totalQuestions,
1352
+ questionnaireFreeText,
1353
+ questionnaireDraft,
1354
+ onQuestionnaireDraftChange,
1355
+ onQuestionnaireSelect,
1356
+ onQuestionnaireOther,
1357
+ onQuestionnaireDraftSubmit,
1358
+ inputKey,
1359
+ inputValue,
1360
+ promptPlaceholder,
1361
+ onInputChange,
1362
+ onPromptSubmit,
1363
+ commandOverlay
1364
+ }) {
1365
+ return /* @__PURE__ */ jsxs7(
1366
+ FullScreenBox,
1367
+ {
1368
+ flexDirection: "column",
1369
+ display: "flex",
1370
+ alignItems: "center",
1371
+ justifyContent: "flex-start",
1372
+ backgroundColor: BG_BLACK,
1373
+ padding: WINDOW_PAD,
1374
+ children: [
1375
+ /* @__PURE__ */ jsxs7(
1376
+ Box7,
1377
+ {
1378
+ width: "100%",
1379
+ height: "100%",
1380
+ display: "flex",
1381
+ flexDirection: "column",
1382
+ justifyContent: "center",
1383
+ alignItems: "center",
1384
+ children: [
1385
+ /* @__PURE__ */ jsx8(BigText, { text: "Nerve" }),
1386
+ questionnaireActive ? /* @__PURE__ */ jsxs7(Box7, { width: "100%", display: "flex", flexDirection: "column", alignItems: "center", children: [
1387
+ /* @__PURE__ */ jsx8(
1388
+ QuestionnaireBox,
1389
+ {
1390
+ question,
1391
+ current: currentQuestion,
1392
+ total: totalQuestions,
1393
+ onSelect: onQuestionnaireSelect,
1394
+ freeText: questionnaireFreeText,
1395
+ onOther: onQuestionnaireOther
1396
+ }
1397
+ ),
1398
+ questionnaireFreeText && /* @__PURE__ */ jsx8(
1399
+ PromptBox,
1400
+ {
1401
+ value: questionnaireDraft,
1402
+ onChange: onQuestionnaireDraftChange,
1403
+ onSubmit: onQuestionnaireDraftSubmit,
1404
+ placeholder: "Type your answer\u2026 (Esc to go back)",
1405
+ width: logoPromptWidth,
1406
+ maxContentRows: promptMaxRows,
1407
+ bordered: true
1408
+ },
1409
+ `q-other-logo-${currentQuestion}`
1410
+ )
1411
+ ] }) : /* @__PURE__ */ jsxs7(Fragment, { children: [
1412
+ suggestionOpen && /* @__PURE__ */ jsx8(
1413
+ Box7,
1414
+ {
1415
+ width: Math.min(LOGO_PROMPT_WIDTH, innerWidth),
1416
+ display: "flex",
1417
+ flexDirection: "column",
1418
+ children: /* @__PURE__ */ jsx8(SuggestionBox, { commands: commandMatches, selectedIndex: selectedIdx })
1419
+ }
1420
+ ),
1421
+ /* @__PURE__ */ jsx8(
1422
+ PromptBox,
1423
+ {
1424
+ value: inputValue,
1425
+ onChange: onInputChange,
1426
+ onSubmit: onPromptSubmit,
1427
+ placeholder: promptPlaceholder,
1428
+ width: logoPromptWidth,
1429
+ maxContentRows: promptMaxRows,
1430
+ bordered: false
1431
+ },
1432
+ inputKey
1433
+ )
1434
+ ] })
1435
+ ]
1436
+ }
1437
+ ),
1438
+ commandOverlay
1439
+ ]
1440
+ }
1441
+ );
1442
+ }
1443
+
1444
+ // src/ui/task_pane.tsx
1445
+ import { Spinner } from "@inkjs/ui";
1446
+ import { Box as Box8, Text as Text7 } from "ink";
1447
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1448
+ function buildTaskLines(tasks, task_text_width) {
1449
+ if (tasks.length === 0) {
1450
+ return [];
1451
+ }
1452
+ const lines = [];
1453
+ tasks.forEach((category, category_index) => {
1454
+ lines.push({
1455
+ type: "category",
1456
+ key: `c-${category_index}`,
1457
+ label: category.name,
1458
+ status: category.status,
1459
+ waitsOn: category.status === "pending" && category.depends_on.length > 0 ? category.depends_on.join(", ") : ""
1460
+ });
1461
+ category.tasks.forEach((task, task_index) => {
1462
+ const wrapped = wrapLines(task.description, task_text_width);
1463
+ wrapped.forEach((text, line_index) => {
1464
+ lines.push({
1465
+ type: "task_line",
1466
+ key: `t-${category_index}-${task_index}:${line_index}`,
1467
+ status: task.status,
1468
+ text,
1469
+ showGutter: line_index === 0
1470
+ });
1471
+ });
1472
+ });
1473
+ lines.push({ type: "blank", key: `b-${category_index}` });
1474
+ });
1475
+ return lines;
1476
+ }
1477
+ function TaskPane({
1478
+ taskWindow,
1479
+ taskScroll,
1480
+ taskGutterWidth,
1481
+ width,
1482
+ cwd,
1483
+ modelLabel
1484
+ }) {
1485
+ return /* @__PURE__ */ jsxs8(Box8, { width, height: "100%", flexDirection: "column", display: "flex", flexShrink: 0, children: [
1486
+ /* @__PURE__ */ jsxs8(
1487
+ Box8,
1488
+ {
1489
+ width: "100%",
1490
+ flexGrow: 1,
1491
+ flexShrink: 1,
1492
+ minHeight: 0,
1493
+ flexDirection: "column",
1494
+ display: "flex",
1495
+ backgroundColor: BG_PANEL,
1496
+ paddingX: 1,
1497
+ paddingY: 1,
1498
+ overflow: "hidden",
1499
+ children: [
1500
+ /* @__PURE__ */ jsx9(Text7, { bold: true, children: "Tasks" }),
1501
+ taskWindow.visible.length === 0 ? /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: "No tasks yet" }) : taskWindow.visible.map((line) => {
1502
+ if (line.type === "blank") {
1503
+ return /* @__PURE__ */ jsx9(Text7, { children: " " }, line.key);
1504
+ }
1505
+ if (line.type === "category") {
1506
+ const marker = line.status === "done" ? "\u2713 " : line.status === "failed" ? "\u2717 " : line.status === "blocked" ? "\u2298 " : "\u25B6 ";
1507
+ const color = line.status === "done" ? "green" : line.status === "failed" ? "red" : line.status === "blocked" ? "yellow" : void 0;
1508
+ return /* @__PURE__ */ jsxs8(Text7, { bold: true, color, children: [
1509
+ marker,
1510
+ line.label,
1511
+ line.status === "running" ? " \u2026" : "",
1512
+ line.waitsOn ? /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: ` (waits: ${line.waitsOn})` }) : null
1513
+ ] }, line.key);
1514
+ }
1515
+ if (line.status === "running") {
1516
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "row", children: [
1517
+ /* @__PURE__ */ jsx9(Box8, { width: taskGutterWidth, flexShrink: 0, children: line.showGutter ? /* @__PURE__ */ jsx9(Spinner, {}) : null }),
1518
+ /* @__PURE__ */ jsx9(Text7, { color: "green", children: line.text })
1519
+ ] }, line.key);
1520
+ }
1521
+ if (line.status === "done" || line.status === "failed") {
1522
+ const failed = line.status === "failed";
1523
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "row", children: [
1524
+ /* @__PURE__ */ jsx9(Box8, { width: taskGutterWidth, flexShrink: 0, children: /* @__PURE__ */ jsx9(Text7, { color: failed ? "red" : "green", children: line.showGutter ? failed ? "\u2717" : "\u2713" : " " }) }),
1525
+ /* @__PURE__ */ jsx9(Text7, { dimColor: failed ? void 0 : true, color: failed ? "red" : void 0, children: line.text })
1526
+ ] }, line.key);
1527
+ }
1528
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "row", children: [
1529
+ /* @__PURE__ */ jsx9(Box8, { width: taskGutterWidth, flexShrink: 0, children: /* @__PURE__ */ jsx9(Text7, { children: " " }) }),
1530
+ /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: line.text })
1531
+ ] }, line.key);
1532
+ }),
1533
+ taskWindow.maxScroll > 0 && /* @__PURE__ */ jsxs8(Text7, { dimColor: true, children: [
1534
+ taskScroll > 0 ? `\u2191 ${taskScroll} ` : "",
1535
+ taskScroll < taskWindow.maxScroll ? "\u2193" : ""
1536
+ ] })
1537
+ ]
1538
+ }
1539
+ ),
1540
+ /* @__PURE__ */ jsx9(StatusFooter, { width, cwd, modelLabel })
1541
+ ] });
1542
+ }
1543
+
1544
+ // src/systemconfig/limits.ts
1545
+ var COMMAND_TIMEOUT_MS = 3e4;
1546
+ var OUTPUT_CHAR_CAP = 8e3;
1547
+ var MAX_READ_BYTES = 1e5;
1548
+ var DEFAULT_READ_LIMIT = 500;
1549
+ var GREP_MAX_MATCHES = 50;
1550
+ var GLOB_MAX_RESULTS = 100;
1551
+ var IGNORED_DIR_NAMES = /* @__PURE__ */ new Set([
1552
+ ".git",
1553
+ "node_modules",
1554
+ "__pycache__",
1555
+ ".venv",
1556
+ "venv",
1557
+ "dist",
1558
+ "build",
1559
+ ".next",
1560
+ ".pytest_cache",
1561
+ ".ruff_cache"
1562
+ ]);
1563
+
1564
+ // src/tool/args.ts
1565
+ function asString(value) {
1566
+ return typeof value === "string" ? value : "";
1567
+ }
1568
+ function asOptionalString(value) {
1569
+ return typeof value === "string" ? value : null;
1570
+ }
1571
+ function asNumber(value, fallback) {
1572
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
1573
+ }
1574
+
1575
+ // src/tool/file_ops.ts
1576
+ import { lstat, mkdir as mkdir2, readdir, readFile as readFile3, stat, writeFile as writeFile3 } from "fs/promises";
1577
+ import { basename, dirname, join as join2 } from "path";
1578
+
1579
+ // src/tool/workspace.ts
1580
+ import { homedir } from "os";
1581
+ import { isAbsolute, resolve, sep } from "path";
1582
+ function getWorkspaceRoot() {
1583
+ return resolve(process.cwd());
1584
+ }
1585
+ function resolveSafePath(user_path) {
1586
+ if (!user_path?.trim()) {
1587
+ return { error: "Error: path must be a non-empty string" };
1588
+ }
1589
+ const root = getWorkspaceRoot();
1590
+ let candidate = user_path;
1591
+ if (candidate === "~") {
1592
+ candidate = homedir();
1593
+ } else if (candidate.startsWith("~/") || candidate.startsWith(`~${sep}`)) {
1594
+ candidate = `${homedir()}/${candidate.slice(2)}`;
1595
+ }
1596
+ try {
1597
+ const resolved = isAbsolute(candidate) ? resolve(candidate) : resolve(root, candidate);
1598
+ if (resolved !== root && !resolved.startsWith(`${root}${sep}`)) {
1599
+ return { error: `Error: path outside workspace: ${user_path}` };
1600
+ }
1601
+ return { path: resolved };
1602
+ } catch (e) {
1603
+ return {
1604
+ error: `Error: cannot resolve path '${user_path}': ${String(e)}`
1605
+ };
1606
+ }
1607
+ }
1608
+
1609
+ // src/tool/file_ops.ts
1610
+ async function writeFileTool(file_path, content, mode) {
1611
+ if (mode !== "overwrite" && mode !== "append") {
1612
+ return `Error: mode must be 'overwrite' or 'append', got: ${mode}`;
1613
+ }
1614
+ const safe = resolveSafePath(file_path);
1615
+ if ("error" in safe) return safe.error;
1616
+ await mkdir2(dirname(safe.path), { recursive: true });
1617
+ await writeFile3(safe.path, content, {
1618
+ encoding: "utf8",
1619
+ flag: mode === "overwrite" ? "w" : "a"
1620
+ });
1621
+ return `Successfully ${mode === "overwrite" ? "overwritten" : "appended to"} file: ${file_path}`;
1622
+ }
1623
+ async function editTool(file_path, old_string, new_string) {
1624
+ if (!old_string) return "Error: old_string must be non-empty";
1625
+ const safe = resolveSafePath(file_path);
1626
+ if ("error" in safe) return safe.error;
1627
+ try {
1628
+ await lstat(safe.path);
1629
+ } catch {
1630
+ return `Error: Path does not exist: ${file_path}`;
1631
+ }
1632
+ if ((await stat(safe.path)).isDirectory()) {
1633
+ return `Error: Path is a directory, not a file: ${file_path}`;
1634
+ }
1635
+ const content = await readFile3(safe.path, "utf8");
1636
+ const count = content.split(old_string).length - 1;
1637
+ if (count === 0) return "Error: old_string not found in file";
1638
+ if (count > 1) {
1639
+ return `Error: old_string found ${count} times; add more context to make it unique`;
1640
+ }
1641
+ await writeFile3(safe.path, content.replace(old_string, new_string), "utf8");
1642
+ return `Successfully edited file: ${file_path}`;
1643
+ }
1644
+ function splitLines(text) {
1645
+ const lines = text.split(/\r\n|\r|\n/);
1646
+ if (text.endsWith("\n") || text.endsWith("\r")) lines.pop();
1647
+ return lines;
1648
+ }
1649
+ async function readFileTool(file_path, offset, limit) {
1650
+ const safe = resolveSafePath(file_path);
1651
+ if ("error" in safe) return safe.error;
1652
+ try {
1653
+ await lstat(safe.path);
1654
+ } catch {
1655
+ return `Error: Path does not exist: ${file_path}`;
1656
+ }
1657
+ if ((await stat(safe.path)).isDirectory()) {
1658
+ return `Error: Path is a directory, not a file: ${file_path}`;
1659
+ }
1660
+ if (offset < 1) return "Error: offset must be >= 1";
1661
+ if (limit < 1) return "Error: limit must be >= 1";
1662
+ const size = (await stat(safe.path)).size;
1663
+ const truncated_file = size > MAX_READ_BYTES;
1664
+ let raw = await readFile3(safe.path, "utf8");
1665
+ if (truncated_file) raw = raw.slice(0, MAX_READ_BYTES);
1666
+ const lines = splitLines(raw);
1667
+ const window = lines.slice(offset - 1, offset - 1 + limit);
1668
+ if (window.length === 0) {
1669
+ return `Error: offset ${offset} is past end of file (${lines.length} lines)`;
1670
+ }
1671
+ let numbered = window.map((line, i) => `${offset + i}: ${line}`).join("\n");
1672
+ const notes = [];
1673
+ if (truncated_file) {
1674
+ notes.push(`Note: file truncated at ${MAX_READ_BYTES} bytes for reading.`);
1675
+ }
1676
+ const end = offset - 1 + window.length;
1677
+ if (end < lines.length) {
1678
+ notes.push(`Note: showed lines ${offset}-${end}; more lines remain (use offset/limit).`);
1679
+ }
1680
+ if (notes.length > 0) numbered = `${numbered}
1681
+ ${notes.join("\n")}`;
1682
+ return numbered;
1683
+ }
1684
+ var TREE_ELBOW = "\u2514\u2500\u2500 ";
1685
+ var TREE_TEE = "\u251C\u2500\u2500 ";
1686
+ var TREE_PIPE_PREFIX = "\u2502 ";
1687
+ var TREE_SPACE_PREFIX = " ";
1688
+ async function walkTree(directory, prefix, lines) {
1689
+ let names;
1690
+ try {
1691
+ names = await readdir(directory);
1692
+ } catch {
1693
+ return;
1694
+ }
1695
+ const entries = names.filter((name) => !IGNORED_DIR_NAMES.has(name)).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
1696
+ if (entries.length === 0) return;
1697
+ const last_index = entries.length - 1;
1698
+ for (const [index, name] of entries.entries()) {
1699
+ const is_last = index === last_index;
1700
+ const entry_path = join2(directory, name);
1701
+ let is_dir = false;
1702
+ try {
1703
+ is_dir = (await lstat(entry_path)).isDirectory();
1704
+ } catch {
1705
+ continue;
1706
+ }
1707
+ const connector = is_last ? TREE_ELBOW : TREE_TEE;
1708
+ lines.push(`${prefix}${connector}${name}${is_dir ? "/" : ""}`);
1709
+ if (is_dir) {
1710
+ const child_prefix = is_last ? TREE_SPACE_PREFIX : TREE_PIPE_PREFIX;
1711
+ await walkTree(entry_path, `${prefix}${child_prefix}`, lines);
1712
+ if (!is_last) lines.push(`${prefix}\u2502`);
1713
+ }
1714
+ }
1715
+ }
1716
+ async function listDirTool(directory_path) {
1717
+ const safe = resolveSafePath(directory_path);
1718
+ if ("error" in safe) return safe.error;
1719
+ try {
1720
+ await lstat(safe.path);
1721
+ } catch {
1722
+ return `Error: Path does not exist: ${directory_path}`;
1723
+ }
1724
+ if (!(await stat(safe.path)).isDirectory()) {
1725
+ return `Error: Path is not a directory: ${directory_path}`;
1726
+ }
1727
+ const root_name = basename(safe.path) || safe.path;
1728
+ const lines = [`${root_name}/`];
1729
+ let names = [];
1730
+ try {
1731
+ names = await readdir(safe.path);
1732
+ } catch {
1733
+ names = [];
1734
+ }
1735
+ if (names.some((name) => !IGNORED_DIR_NAMES.has(name))) lines.push("\u2502");
1736
+ await walkTree(safe.path, "", lines);
1737
+ return lines.join("\n");
1738
+ }
1739
+
1740
+ // src/tool/search_ops.ts
1741
+ import { glob as fsGlob, readFile as readFile4, stat as stat2 } from "fs/promises";
1742
+ import { basename as basename2, relative, resolve as resolve2, sep as sep2 } from "path";
1743
+ function hasIgnoredPart(relative_path) {
1744
+ return relative_path.split(sep2).some((part) => IGNORED_DIR_NAMES.has(part));
1745
+ }
1746
+ function recursivePattern(pattern) {
1747
+ return pattern.startsWith("**/") ? pattern : `**/${pattern}`;
1748
+ }
1749
+ async function collectMatchingFiles(root, pattern) {
1750
+ const matches = [];
1751
+ for await (const entry of fsGlob(recursivePattern(pattern), { cwd: root })) {
1752
+ const full = resolve2(root, entry);
1753
+ if (hasIgnoredPart(relative(root, full))) continue;
1754
+ try {
1755
+ if ((await stat2(full)).isFile()) matches.push(full);
1756
+ } catch {
1757
+ }
1758
+ }
1759
+ return matches.sort();
1760
+ }
1761
+ async function grepTool(pattern, path, file_glob) {
1762
+ let regex;
1763
+ try {
1764
+ regex = new RegExp(pattern);
1765
+ } catch (e) {
1766
+ return `Error: invalid regex pattern: ${e instanceof Error ? e.message : String(e)}`;
1767
+ }
1768
+ const safe = resolveSafePath(path);
1769
+ if ("error" in safe) return safe.error;
1770
+ let root_stat;
1771
+ try {
1772
+ root_stat = await stat2(safe.path);
1773
+ } catch {
1774
+ return `Error: Path does not exist: ${path}`;
1775
+ }
1776
+ let files;
1777
+ if (root_stat.isFile()) {
1778
+ files = [safe.path];
1779
+ } else if (root_stat.isDirectory()) {
1780
+ files = await collectMatchingFiles(safe.path, file_glob ?? "*");
1781
+ } else {
1782
+ return `Error: Path does not exist: ${path}`;
1783
+ }
1784
+ const matches = [];
1785
+ for (const file_path of files) {
1786
+ let text;
1787
+ try {
1788
+ text = await readFile4(file_path, "utf8");
1789
+ } catch {
1790
+ continue;
1791
+ }
1792
+ const lines = text.split(/\r\n|\r|\n/);
1793
+ if (text.endsWith("\n") || text.endsWith("\r")) lines.pop();
1794
+ for (const [index, line] of lines.entries()) {
1795
+ if (!regex.test(line)) continue;
1796
+ const label = root_stat.isFile() ? basename2(file_path) : relative(safe.path, file_path);
1797
+ matches.push(`${label}:${index + 1}: ${line.trimEnd()}`);
1798
+ if (matches.length >= GREP_MAX_MATCHES) {
1799
+ return `${matches.join("\n")}
1800
+ ... [stopped at ${GREP_MAX_MATCHES} matches]`;
1801
+ }
1802
+ }
1803
+ }
1804
+ return matches.length > 0 ? matches.join("\n") : "No matches found";
1805
+ }
1806
+ async function globTool(pattern, path) {
1807
+ const safe = resolveSafePath(path);
1808
+ if ("error" in safe) return safe.error;
1809
+ let root_stat;
1810
+ try {
1811
+ root_stat = await stat2(safe.path);
1812
+ } catch {
1813
+ return `Error: Path is not a directory: ${path}`;
1814
+ }
1815
+ if (!root_stat.isDirectory()) {
1816
+ return `Error: Path is not a directory: ${path}`;
1817
+ }
1818
+ const results = [];
1819
+ for await (const entry of fsGlob(pattern, { cwd: safe.path })) {
1820
+ const full = resolve2(safe.path, entry);
1821
+ if (hasIgnoredPart(relative(safe.path, full))) continue;
1822
+ try {
1823
+ const entry_stat = await stat2(full);
1824
+ if (entry_stat.isFile()) {
1825
+ results.push({ path: full, modified: entry_stat.mtimeMs });
1826
+ }
1827
+ } catch {
1828
+ }
1829
+ }
1830
+ if (results.length === 0) return "No files matched";
1831
+ results.sort((a, b) => b.modified - a.modified);
1832
+ const shown = results.slice(0, GLOB_MAX_RESULTS);
1833
+ const lines = shown.map((entry) => entry.path);
1834
+ if (results.length > GLOB_MAX_RESULTS) {
1835
+ lines.push(`... [showing ${GLOB_MAX_RESULTS} of ${results.length} matches]`);
1836
+ }
1837
+ return lines.join("\n");
1838
+ }
1839
+
1840
+ // src/tool/shell.ts
1841
+ import { exec } from "child_process";
1842
+ function truncateOutput(text, cap) {
1843
+ if (text.length <= cap) return text;
1844
+ return `${text.slice(0, cap)}
1845
+ ... [truncated, ${text.length - cap} more characters]`;
1846
+ }
1847
+ function runTerminalCommand(bash_command) {
1848
+ return new Promise((resolve_promise) => {
1849
+ let settled = false;
1850
+ const finish = (result) => {
1851
+ if (settled) return;
1852
+ settled = true;
1853
+ resolve_promise(result);
1854
+ };
1855
+ const child = exec(
1856
+ bash_command,
1857
+ { cwd: process.cwd(), maxBuffer: 10 * 1024 * 1024 },
1858
+ (error, stdout, stderr) => {
1859
+ if (settled) return;
1860
+ if (error && typeof error.code !== "number") {
1861
+ finish(`Failed to execute the command: ${error.message}`);
1862
+ return;
1863
+ }
1864
+ const exit_code = error ? error.code : 0;
1865
+ const parts = [`Exit code: ${exit_code}`];
1866
+ if (stdout) {
1867
+ parts.push(`stdout:
1868
+ ${truncateOutput(stdout, OUTPUT_CHAR_CAP)}`);
1869
+ }
1870
+ if (stderr) {
1871
+ parts.push(`stderr:
1872
+ ${truncateOutput(stderr, OUTPUT_CHAR_CAP)}`);
1873
+ }
1874
+ if (!stdout && !stderr) parts.push("(no output)");
1875
+ finish(parts.join("\n"));
1876
+ }
1877
+ );
1878
+ const timer = setTimeout(() => {
1879
+ child.kill();
1880
+ finish(`Error: command timed out after ${COMMAND_TIMEOUT_MS / 1e3}s`);
1881
+ }, COMMAND_TIMEOUT_MS);
1882
+ child.on("close", () => clearTimeout(timer));
1883
+ });
1884
+ }
1885
+
1886
+ // src/tool/executor.ts
1887
+ async function executeToolRequest(request) {
1888
+ const args = request.args ?? {};
1889
+ try {
1890
+ switch (request.tool) {
1891
+ case "write":
1892
+ return await writeFileTool(
1893
+ asString(args.file_path),
1894
+ asString(args.content),
1895
+ args.mode === void 0 ? "overwrite" : asString(args.mode)
1896
+ );
1897
+ case "edit":
1898
+ return await editTool(
1899
+ asString(args.file_path),
1900
+ asString(args.old_string),
1901
+ asString(args.new_string)
1902
+ );
1903
+ case "readFile":
1904
+ return await readFileTool(
1905
+ asString(args.file_path),
1906
+ asNumber(args.offset, 1),
1907
+ asNumber(args.limit, DEFAULT_READ_LIMIT)
1908
+ );
1909
+ case "listDir":
1910
+ return await listDirTool(asString(args.directory_path));
1911
+ case "grep":
1912
+ return await grepTool(
1913
+ asString(args.pattern),
1914
+ asString(args.path) || ".",
1915
+ asOptionalString(args.file_glob)
1916
+ );
1917
+ case "glob":
1918
+ return await globTool(asString(args.pattern), asString(args.path) || ".");
1919
+ case "terminalCommand":
1920
+ return await runTerminalCommand(asString(args.bash_command));
1921
+ default:
1922
+ return `Error: unknown tool: ${request.tool}`;
1923
+ }
1924
+ } catch (e) {
1925
+ return `Error: ${e instanceof Error ? e.message : String(e)}`;
1926
+ }
1927
+ }
1928
+
1929
+ // src/socket/listener.ts
1930
+ socket.on("main_agent_response", (data) => {
1931
+ const entry = { role: "assistant", content: data };
1932
+ saveDisplayHistory(entry);
1933
+ display_handler?.(entry);
1934
+ });
1935
+ socket.on("task_update", (data) => {
1936
+ saveTaskUpdate(data);
1937
+ task_update_handler?.(data);
1938
+ });
1939
+ socket.on("subagent_response", (data) => {
1940
+ subagent_response_handler?.(data);
1941
+ });
1942
+ socket.on("connection_status", (data) => {
1943
+ connection_status_handler?.(data);
1944
+ });
1945
+ socket.on("questionnaire", (data) => {
1946
+ questionnaire_handler?.(data);
1947
+ });
1948
+ socket.on("agent_error", (data) => {
1949
+ error_handler?.(data);
1950
+ emitAgentErrorResponse(data);
1951
+ });
1952
+ socket.on("rate_limited", (data) => {
1953
+ rate_limited_handler?.(data);
1954
+ });
1955
+ socket.on("tool_request", (data) => {
1956
+ void executeToolRequest(data).catch((e) => `Error: ${e instanceof Error ? e.message : String(e)}`).then((output) => {
1957
+ emitToolResult({ id: data.id, ok: !output.startsWith("Error:"), output });
1958
+ });
1959
+ });
1960
+ socket.on("disconnect", async () => {
1961
+ await writeUserDataToFile();
1962
+ });
1963
+
1964
+ // src/app.tsx
1965
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1966
+ var PROMPT_PLACEHOLDER = "SPAM your prompts here... (or /model, /session)";
1967
+ function App() {
1968
+ const [logo_show, setLogoShow] = useState2(true);
1969
+ const [displays, setDisplays] = useState2([]);
1970
+ const [tasks, setTasks] = useState2([]);
1971
+ const [display_scroll, setDisplayScroll] = useState2(0);
1972
+ const [task_scroll, setTaskScroll] = useState2(0);
1973
+ const [, setConnectionStatus] = useState2(false);
1974
+ const [questions, setQuestions] = useState2([]);
1975
+ const [current_question, setCurrentQuestion] = useState2(0);
1976
+ const [answers, setAnswers] = useState2([]);
1977
+ const [questionnaire_free_text, setQuestionnaireFreeText] = useState2(false);
1978
+ const [questionnaire_draft, setQuestionnaireDraft] = useState2("");
1979
+ const questions_ref = useRef3([]);
1980
+ const questionnaire_active = questions.length > 0 && current_question < questions.length;
1981
+ const [command_mode, setCommandMode] = useState2({
1982
+ type: "none"
1983
+ });
1984
+ const [input_value, setInputValue] = useState2("");
1985
+ const [selected_idx, setSelectedIdx] = useState2(0);
1986
+ const [input_key, setInputKey] = useState2(0);
1987
+ const { height: screen_height, width: screen_width } = useScreenSize();
1988
+ const command_matches = command_mode.type === "none" && !questionnaire_active ? filterCommands(input_value) : [];
1989
+ const suggestion_open = command_matches.length > 0;
1990
+ const {
1991
+ inner_width,
1992
+ inner_height,
1993
+ show_task_bar,
1994
+ task_pane_width,
1995
+ chat_gap,
1996
+ chat_pane_width,
1997
+ chat_content_width,
1998
+ task_gutter_width,
1999
+ task_text_width,
2000
+ logo_prompt_width,
2001
+ task_viewport_rows,
2002
+ prompt_max_rows,
2003
+ prompt_box_height,
2004
+ display_viewport_rows,
2005
+ prompt_scrollable,
2006
+ entry_content_width
2007
+ } = useLayout({
2008
+ screen_width,
2009
+ screen_height,
2010
+ command_matches,
2011
+ questionnaire_active,
2012
+ questions,
2013
+ current_question,
2014
+ input_value,
2015
+ questionnaire_free_text,
2016
+ questionnaire_draft
2017
+ });
2018
+ const status_cwd = process.cwd();
2019
+ const status_model_label = user_data ? getModelLabel(user_data.main_agent_id) : "\u2014";
2020
+ useEffect4(() => {
2021
+ questions_ref.current = questions;
2022
+ }, [questions]);
2023
+ const chat_lines = useMemo2(
2024
+ () => flattenDisplayEntries(displays, entry_content_width),
2025
+ [displays, entry_content_width]
2026
+ );
2027
+ const chat_rows = useMemo2(() => displayRowsFromEntries(chat_lines), [chat_lines]);
2028
+ const task_lines = useMemo2(
2029
+ () => buildTaskLines(tasks, task_text_width),
2030
+ [tasks, task_text_width]
2031
+ );
2032
+ const chat_window = windowFromBottom(
2033
+ chat_rows,
2034
+ display_viewport_rows - (display_scroll > 0 ? 1 : 0),
2035
+ display_scroll
2036
+ );
2037
+ const chat_blocks = groupRowsIntoBlocks(chat_window.visible);
2038
+ const task_window = windowFromTop(task_lines, task_viewport_rows, task_scroll);
2039
+ const scrollChat = (delta) => {
2040
+ setDisplayScroll((prev) => Math.max(0, Math.min(chat_window.maxScroll, prev + delta)));
2041
+ };
2042
+ const scrollTasks = (delta) => {
2043
+ setTaskScroll((prev) => Math.max(0, Math.min(task_window.maxScroll, prev + delta)));
2044
+ };
2045
+ const cancelActiveCommand = () => {
2046
+ setCommandMode({ type: "none" });
2047
+ setLogoShow(false);
2048
+ setInputValue("");
2049
+ setSelectedIdx(0);
2050
+ setInputKey((prev) => prev + 1);
2051
+ };
2052
+ useInput2(
2053
+ (input, key) => {
2054
+ void input;
2055
+ if (key.escape && questionnaire_free_text) {
2056
+ setQuestionnaireFreeText(false);
2057
+ setQuestionnaireDraft("");
2058
+ return;
2059
+ }
2060
+ if (key.escape && command_mode.type !== "none") {
2061
+ cancelActiveCommand();
2062
+ return;
2063
+ }
2064
+ if (suggestion_open) {
2065
+ if (key.upArrow) {
2066
+ setSelectedIdx((prev) => prev <= 0 ? command_matches.length - 1 : prev - 1);
2067
+ } else if (key.downArrow) {
2068
+ setSelectedIdx((prev) => prev >= command_matches.length - 1 ? 0 : prev + 1);
2069
+ } else if (key.tab) {
2070
+ const entry = command_matches[selected_idx];
2071
+ if (entry) {
2072
+ setInputValue(entry.name);
2073
+ setInputKey((prev) => prev + 1);
2074
+ }
2075
+ }
2076
+ return;
2077
+ }
2078
+ if (command_mode.type !== "none" || logo_show || questionnaire_active) {
2079
+ return;
2080
+ }
2081
+ if (key.ctrl && key.upArrow) {
2082
+ scrollTasks(-1);
2083
+ } else if (key.ctrl && key.downArrow) {
2084
+ scrollTasks(1);
2085
+ } else if (key.pageUp) {
2086
+ scrollChat(display_viewport_rows);
2087
+ } else if (key.pageDown) {
2088
+ scrollChat(-display_viewport_rows);
2089
+ } else if (key.home) {
2090
+ scrollChat(chat_window.maxScroll);
2091
+ } else if (key.end) {
2092
+ scrollChat(-chat_window.maxScroll);
2093
+ } else if (key.upArrow) {
2094
+ if (prompt_scrollable) return;
2095
+ scrollChat(1);
2096
+ } else if (key.downArrow) {
2097
+ if (prompt_scrollable) return;
2098
+ scrollChat(-1);
2099
+ }
2100
+ },
2101
+ { isActive: true }
2102
+ );
2103
+ useBracketedPaste();
2104
+ useMouseWheel(!logo_show, (direction, x) => {
2105
+ if (command_mode.type !== "none") {
2106
+ return;
2107
+ }
2108
+ const over_tasks = show_task_bar && x > chat_pane_width;
2109
+ const delta = direction === "up" ? 3 : -3;
2110
+ if (over_tasks) {
2111
+ scrollTasks(-delta);
2112
+ } else {
2113
+ scrollChat(delta);
2114
+ }
2115
+ });
2116
+ const handleInputChange = (value) => {
2117
+ setInputValue(value);
2118
+ setSelectedIdx(0);
2119
+ };
2120
+ const completeToSelectedCommand = (value) => {
2121
+ if (!value.startsWith("/")) return value;
2122
+ const exact = command_matches.find((entry) => entry.name === value);
2123
+ if (exact) return exact.name;
2124
+ const selected = command_matches[selected_idx] ?? command_matches[0];
2125
+ return selected ? selected.name : value;
2126
+ };
2127
+ useEffect4(() => {
2128
+ const flushAndExit = async () => {
2129
+ try {
2130
+ await writeUserDataToFile();
2131
+ } finally {
2132
+ process.exit(0);
2133
+ }
2134
+ };
2135
+ process.on("SIGINT", flushAndExit);
2136
+ process.on("SIGTERM", flushAndExit);
2137
+ (async () => {
2138
+ await ensureConfigFile();
2139
+ const data = await readConfig();
2140
+ setUserData(data);
2141
+ setTasks(user_data?.categories ?? []);
2142
+ setDisplayScroll(0);
2143
+ setTaskScroll(0);
2144
+ connectSocket();
2145
+ })();
2146
+ return () => {
2147
+ process.off("SIGINT", flushAndExit);
2148
+ process.off("SIGTERM", flushAndExit);
2149
+ };
2150
+ }, []);
2151
+ useEffect4(() => {
2152
+ onDisplay((entry) => {
2153
+ setDisplays((prev) => [...prev, entry]);
2154
+ });
2155
+ onTaskUpdate(() => {
2156
+ setTasks(user_data?.categories ?? []);
2157
+ });
2158
+ onSubagentResponse((data) => {
2159
+ const entry = {
2160
+ role: "system",
2161
+ content: `${data.status === "done" ? "\u2713" : "\u2717"} [${data.category}] ${data.report}`
2162
+ };
2163
+ saveDisplayHistory(entry);
2164
+ setDisplays((prev) => [...prev, entry]);
2165
+ });
2166
+ onConnectionStatus((connected) => {
2167
+ setConnectionStatus(connected);
2168
+ if (!connected) {
2169
+ setDisplays((prev) => [
2170
+ ...prev,
2171
+ {
2172
+ role: "system",
2173
+ content: "Cannot connect to model \u2014 set API key with /model"
2174
+ }
2175
+ ]);
2176
+ }
2177
+ });
2178
+ onQuestionnaire((qs) => {
2179
+ const prev = questions_ref.current;
2180
+ if (prev.length > 0 && prev.length === qs.length && JSON.stringify(prev) === JSON.stringify(qs)) {
2181
+ return;
2182
+ }
2183
+ questions_ref.current = qs;
2184
+ setQuestions(qs);
2185
+ setCurrentQuestion(0);
2186
+ setAnswers([]);
2187
+ setQuestionnaireFreeText(false);
2188
+ setQuestionnaireDraft("");
2189
+ });
2190
+ onError((message) => {
2191
+ setDisplays((prev) => [...prev, { role: "error", content: message }]);
2192
+ });
2193
+ onRateLimited((data) => {
2194
+ setDisplays((prev) => [
2195
+ ...prev,
2196
+ { role: "error", content: `Rate limited \u2014 try again in ${data.retry_after}s` }
2197
+ ]);
2198
+ });
2199
+ }, []);
2200
+ const appendSystemNote = (content) => {
2201
+ setDisplays((prev) => [...prev, { role: "system", content }]);
2202
+ };
2203
+ const finishModelSwitch = async (model_id, api_key) => {
2204
+ await persistModelChoice(model_id, api_key);
2205
+ appendSystemNote(`Switched model to ${getModelLabel(model_id)} (id=${model_id})`);
2206
+ setCommandMode({ type: "none" });
2207
+ setLogoShow(false);
2208
+ setInputValue("");
2209
+ setSelectedIdx(0);
2210
+ setInputKey((prev) => prev + 1);
2211
+ };
2212
+ const handleModelSelect = async (value) => {
2213
+ const option = buildModelSelectOptions().find((o) => o.value === value);
2214
+ if (!option) return;
2215
+ const model = MODEL_OPTIONS.find((m) => m.id === option.modelId);
2216
+ if (!model) return;
2217
+ if (isFundedModel(model.id)) {
2218
+ await finishModelSwitch(model.id, "");
2219
+ return;
2220
+ }
2221
+ if (hasApiKeyForModel(model.id)) {
2222
+ const existing_key = user_data?.api_keys[model.id];
2223
+ if (existing_key) {
2224
+ await finishModelSwitch(model.id, existing_key);
2225
+ }
2226
+ } else {
2227
+ setCommandMode({
2228
+ type: "enter_api_key",
2229
+ modelId: model.id,
2230
+ modelLabel: model.label,
2231
+ flow: "model"
2232
+ });
2233
+ }
2234
+ };
2235
+ const handleKeyModelSelect = (value) => {
2236
+ const option = buildModelSelectOptions().find((o) => o.value === value);
2237
+ if (!option) return;
2238
+ const model = MODEL_OPTIONS.find((m) => m.id === option.modelId);
2239
+ if (!model) return;
2240
+ if (isFundedModel(model.id)) {
2241
+ appendSystemNote(`${model.label} is funded by the operator and needs no API key`);
2242
+ setCommandMode({ type: "none" });
2243
+ setInputValue("");
2244
+ setSelectedIdx(0);
2245
+ setInputKey((prev) => prev + 1);
2246
+ return;
2247
+ }
2248
+ setCommandMode({
2249
+ type: "enter_api_key",
2250
+ modelId: model.id,
2251
+ modelLabel: model.label,
2252
+ flow: "key"
2253
+ });
2254
+ };
2255
+ const handleApiKeySubmit = async (value) => {
2256
+ if (command_mode.type !== "enter_api_key") return;
2257
+ const trimmed = value.trim();
2258
+ if (!trimmed) {
2259
+ appendSystemNote(
2260
+ `API key cannot be empty. Try ${command_mode.flow === "key" ? "/key" : "/model"} again.`
2261
+ );
2262
+ setCommandMode({ type: "none" });
2263
+ return;
2264
+ }
2265
+ if (command_mode.flow === "key") {
2266
+ await applyApiKeyChange(command_mode.modelId, trimmed);
2267
+ appendSystemNote(`Updated API key for ${command_mode.modelLabel}`);
2268
+ setCommandMode({ type: "none" });
2269
+ } else {
2270
+ await finishModelSwitch(command_mode.modelId, trimmed);
2271
+ }
2272
+ setInputValue("");
2273
+ setSelectedIdx(0);
2274
+ setInputKey((prev) => prev + 1);
2275
+ };
2276
+ const finishSessionSwitch = () => {
2277
+ setCommandMode({ type: "none" });
2278
+ setLogoShow(false);
2279
+ setInputValue("");
2280
+ setSelectedIdx(0);
2281
+ setInputKey((prev) => prev + 1);
2282
+ };
2283
+ const handleSessionSelect = async (value) => {
2284
+ if (command_mode.type !== "select_session") return;
2285
+ const option = command_mode.sessions.find((s) => s.value === value);
2286
+ if (!option) return;
2287
+ if (option.session_id === null || !option.data) {
2288
+ await startNewSession();
2289
+ setDisplays([]);
2290
+ setTasks([]);
2291
+ setDisplayScroll(0);
2292
+ setTaskScroll(0);
2293
+ appendSystemNote("Started new session");
2294
+ } else {
2295
+ await applySessionChoice(option.session_id, option.data);
2296
+ setDisplays(option.data.history);
2297
+ setTasks(user_data?.categories ?? []);
2298
+ setDisplayScroll(0);
2299
+ setTaskScroll(0);
2300
+ appendSystemNote(`Loaded session ${option.session_id}`);
2301
+ }
2302
+ finishSessionSwitch();
2303
+ };
2304
+ const handlePromptSubmit = (value) => {
2305
+ const trimmed = completeToSelectedCommand(value.trim());
2306
+ setInputValue("");
2307
+ setSelectedIdx(0);
2308
+ setInputKey((prev) => prev + 1);
2309
+ if (command_mode.type === "none" && isModelCommand(trimmed)) {
2310
+ appendSystemNote("/model \u2014 choose a model");
2311
+ setCommandMode({ type: "select_model" });
2312
+ setLogoShow(false);
2313
+ return;
2314
+ }
2315
+ if (command_mode.type === "none" && isKeyCommand(trimmed)) {
2316
+ appendSystemNote("/key \u2014 choose a model to update its API key");
2317
+ setCommandMode({ type: "select_key_model" });
2318
+ setLogoShow(false);
2319
+ return;
2320
+ }
2321
+ if (command_mode.type === "none" && isSessionCommand(trimmed)) {
2322
+ appendSystemNote("/session \u2014 choose a session");
2323
+ setLogoShow(false);
2324
+ void (async () => {
2325
+ const sessions = await loadSessionOptions();
2326
+ const saved_count = sessions.filter((s) => s.session_id !== null).length;
2327
+ if (saved_count === 0) {
2328
+ appendSystemNote("No saved sessions, starting fresh");
2329
+ }
2330
+ setCommandMode({ type: "select_session", sessions });
2331
+ })();
2332
+ return;
2333
+ }
2334
+ if (command_mode.type === "none" && isExitCommand(trimmed)) {
2335
+ void exitApp();
2336
+ return;
2337
+ }
2338
+ if (command_mode.type === "none" && isRestartCommand(trimmed)) {
2339
+ void (async () => {
2340
+ await restartConnection();
2341
+ setDisplays([]);
2342
+ setTasks([]);
2343
+ setDisplayScroll(0);
2344
+ setTaskScroll(0);
2345
+ setQuestions([]);
2346
+ setAnswers([]);
2347
+ setCurrentQuestion(0);
2348
+ setQuestionnaireFreeText(false);
2349
+ setQuestionnaireDraft("");
2350
+ setCommandMode({ type: "none" });
2351
+ setLogoShow(true);
2352
+ setInputValue("");
2353
+ setSelectedIdx(0);
2354
+ setInputKey((prev) => prev + 1);
2355
+ })();
2356
+ return;
2357
+ }
2358
+ if (command_mode.type === "none" && isSlashCommand(trimmed)) {
2359
+ appendSystemNote(
2360
+ `Unknown command: ${trimmed}. Try /model, /key, /session, /exit, or /restart`
2361
+ );
2362
+ setLogoShow(false);
2363
+ return;
2364
+ }
2365
+ if (trimmed) {
2366
+ emitUserPrompt(trimmed);
2367
+ setDisplays((prev) => [...prev, { role: "user", content: trimmed }]);
2368
+ }
2369
+ setLogoShow(false);
2370
+ };
2371
+ const handleQuestionnaireSelect = (value) => {
2372
+ if (!questionnaire_active) return;
2373
+ const q = questions[current_question];
2374
+ const updated = [...answers, { question: q.question, answer: value }];
2375
+ setQuestionnaireFreeText(false);
2376
+ setQuestionnaireDraft("");
2377
+ if (current_question + 1 >= questions.length) {
2378
+ emitQuestionnaireAnswers(updated);
2379
+ for (const a of updated) {
2380
+ const entry = {
2381
+ role: "system",
2382
+ content: `${QUESTION_PREFIX}${a.question}
2383
+ A: ${a.answer}`
2384
+ };
2385
+ setDisplays((prev) => [...prev, entry]);
2386
+ saveDisplayHistory(entry);
2387
+ }
2388
+ setQuestions([]);
2389
+ setAnswers([]);
2390
+ setCurrentQuestion(0);
2391
+ return;
2392
+ }
2393
+ setAnswers(updated);
2394
+ setCurrentQuestion((prev) => prev + 1);
2395
+ };
2396
+ const handleQuestionnaireOther = () => {
2397
+ if (!questionnaire_active) return;
2398
+ setQuestionnaireDraft("");
2399
+ setQuestionnaireFreeText(true);
2400
+ };
2401
+ const handleQuestionnaireDraftSubmit = (value) => {
2402
+ const trimmed = value.trim();
2403
+ if (!trimmed) return;
2404
+ handleQuestionnaireSelect(trimmed);
2405
+ };
2406
+ const command_active = command_mode.type !== "none";
2407
+ const command_overlay = /* @__PURE__ */ jsx10(
2408
+ CommandOverlay,
2409
+ {
2410
+ commandMode: command_mode,
2411
+ screenWidth: inner_width,
2412
+ screenHeight: inner_height,
2413
+ onModelSelect: (val) => {
2414
+ void handleModelSelect(val);
2415
+ },
2416
+ onKeyModelSelect: handleKeyModelSelect,
2417
+ onSessionSelect: (val) => {
2418
+ void handleSessionSelect(val);
2419
+ },
2420
+ onApiKeySubmit: (val) => {
2421
+ void handleApiKeySubmit(val);
2422
+ }
2423
+ }
2424
+ );
2425
+ if (!logo_show) {
2426
+ return /* @__PURE__ */ jsxs9(
2427
+ FullScreenBox2,
2428
+ {
2429
+ flexDirection: "row",
2430
+ display: "flex",
2431
+ backgroundColor: BG_BLACK,
2432
+ padding: WINDOW_PAD,
2433
+ children: [
2434
+ /* @__PURE__ */ jsxs9(
2435
+ Box9,
2436
+ {
2437
+ width: chat_pane_width,
2438
+ height: "100%",
2439
+ flexDirection: "column",
2440
+ display: "flex",
2441
+ marginRight: chat_gap,
2442
+ children: [
2443
+ /* @__PURE__ */ jsx10(
2444
+ ChatViewport,
2445
+ {
2446
+ chatBlocks: chat_blocks,
2447
+ displayScroll: display_scroll,
2448
+ viewportRows: display_viewport_rows
2449
+ }
2450
+ ),
2451
+ suggestion_open && /* @__PURE__ */ jsx10(SuggestionBox, { commands: command_matches, selectedIndex: selected_idx }),
2452
+ questionnaire_active && /* @__PURE__ */ jsx10(
2453
+ QuestionnaireBox,
2454
+ {
2455
+ question: questions[current_question],
2456
+ current: current_question,
2457
+ total: questions.length,
2458
+ onSelect: handleQuestionnaireSelect,
2459
+ freeText: questionnaire_free_text,
2460
+ onOther: handleQuestionnaireOther
2461
+ }
2462
+ ),
2463
+ /* @__PURE__ */ jsx10(
2464
+ Box9,
2465
+ {
2466
+ width: "100%",
2467
+ height: prompt_box_height,
2468
+ display: "flex",
2469
+ flexDirection: "column",
2470
+ flexShrink: 0,
2471
+ children: questionnaire_active && questionnaire_free_text ? /* @__PURE__ */ jsx10(
2472
+ PromptBox,
2473
+ {
2474
+ value: questionnaire_draft,
2475
+ onChange: setQuestionnaireDraft,
2476
+ onSubmit: handleQuestionnaireDraftSubmit,
2477
+ placeholder: "Type your answer\u2026 (Esc to go back)",
2478
+ width: chat_content_width,
2479
+ maxContentRows: prompt_max_rows,
2480
+ bordered: true
2481
+ },
2482
+ `q-other-${current_question}`
2483
+ ) : questionnaire_active ? /* @__PURE__ */ jsx10(Box9, { width: "100%", display: "flex", backgroundColor: BG_PANEL, paddingX: 1, paddingY: 1, children: /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: "Answer the question above\u2026" }) }) : command_active ? /* @__PURE__ */ jsx10(Box9, { width: "100%", display: "flex", backgroundColor: BG_PANEL, paddingX: 1, paddingY: 1, children: /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: "Esc to cancel\u2026" }) }) : /* @__PURE__ */ jsx10(
2484
+ PromptBox,
2485
+ {
2486
+ value: input_value,
2487
+ onChange: handleInputChange,
2488
+ onSubmit: handlePromptSubmit,
2489
+ placeholder: PROMPT_PLACEHOLDER,
2490
+ width: chat_content_width,
2491
+ maxContentRows: prompt_max_rows,
2492
+ bordered: true
2493
+ },
2494
+ input_key
2495
+ )
2496
+ }
2497
+ )
2498
+ ]
2499
+ }
2500
+ ),
2501
+ show_task_bar && /* @__PURE__ */ jsx10(
2502
+ TaskPane,
2503
+ {
2504
+ taskWindow: task_window,
2505
+ taskScroll: task_scroll,
2506
+ taskGutterWidth: task_gutter_width,
2507
+ width: task_pane_width,
2508
+ cwd: status_cwd,
2509
+ modelLabel: status_model_label
2510
+ }
2511
+ ),
2512
+ command_overlay
2513
+ ]
2514
+ }
2515
+ );
2516
+ }
2517
+ return /* @__PURE__ */ jsx10(
2518
+ LogoView,
2519
+ {
2520
+ innerWidth: inner_width,
2521
+ logoPromptWidth: logo_prompt_width,
2522
+ promptMaxRows: prompt_max_rows,
2523
+ suggestionOpen: suggestion_open,
2524
+ commandMatches: command_matches,
2525
+ selectedIdx: selected_idx,
2526
+ questionnaireActive: questionnaire_active,
2527
+ question: questions[current_question],
2528
+ currentQuestion: current_question,
2529
+ totalQuestions: questions.length,
2530
+ questionnaireFreeText: questionnaire_free_text,
2531
+ questionnaireDraft: questionnaire_draft,
2532
+ onQuestionnaireDraftChange: setQuestionnaireDraft,
2533
+ onQuestionnaireSelect: handleQuestionnaireSelect,
2534
+ onQuestionnaireOther: handleQuestionnaireOther,
2535
+ onQuestionnaireDraftSubmit: handleQuestionnaireDraftSubmit,
2536
+ inputKey: input_key,
2537
+ inputValue: input_value,
2538
+ promptPlaceholder: PROMPT_PLACEHOLDER,
2539
+ onInputChange: handleInputChange,
2540
+ onPromptSubmit: handlePromptSubmit,
2541
+ commandOverlay: command_overlay
2542
+ }
2543
+ );
2544
+ }
2545
+ withFullScreen(/* @__PURE__ */ jsx10(App, {}), {
2546
+ kittyKeyboard: { flags: ["disambiguateEscapeCodes"] }
2547
+ }).start();