@rind-ai/cli 0.4.1

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.
@@ -0,0 +1,1111 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createInterface } from "node:readline";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { createRequire } from "node:module";
7
+
8
+ import { AssistantRenderer } from "./assistant-renderer.js";
9
+ import { createAssistantStreamBuffer } from "./assistant-stream-buffer.js";
10
+ import { createCompactContextState } from "./compact-context-state.js";
11
+ import { prepareComposerFrame } from "./composer-terminal.js";
12
+ import { createLineEditor } from "./line-editor.js";
13
+ import { createRuntimeClient, runHelpVersion } from "./runtime-client.js";
14
+ import { createTurnController } from "./turn-controller.js";
15
+ import { createCommandController } from "./command-controller.js";
16
+ import { createBackgroundController } from "./background-controller.js";
17
+ import { createEventController } from "./event-controller.js";
18
+ import { createInputController } from "./input-controller.js";
19
+ import { isInputClosed } from "./input-errors.js";
20
+ import { sigintAction } from "./interrupt-state.js";
21
+ import { createModelMenuState } from "./model-menu-state.js";
22
+ import { createChoiceMenuState } from "./choice-menu-state.js";
23
+ import { createSlashMenuState } from "./slash-menu-state.js";
24
+ import { parseTerminalKey } from "./terminal-key.js";
25
+ import { createTerminalUI } from "./terminal-ui.js";
26
+ import {
27
+ answerPromptText,
28
+ answerPlaceholderText,
29
+ commandResultText,
30
+ goalCommandText,
31
+ assistantHeaderText,
32
+ inputHintText,
33
+ interruptText,
34
+ modelListErrorText,
35
+ modelMenuText,
36
+ choiceMenuText,
37
+ sessionMenuText,
38
+ sessionSwitchedText,
39
+ outputBlockText,
40
+ promptPlaceholderText,
41
+ promptText,
42
+ questionText,
43
+ queuedInputText,
44
+ slashMenuText,
45
+ startupText,
46
+ userInputText,
47
+ } from "./rendering.js";
48
+
49
+ export async function runFrontendCliApp(cliArgs = process.argv.slice(2)) {
50
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
51
+ const repoRoot = path.resolve(scriptDir, "..", "..");
52
+ const python = process.env.RIND_PYTHON || "python";
53
+ const runtimePath = process.env.RIND_RUNTIME_PATH || resolveInstalledRuntime();
54
+
55
+ function resolveInstalledRuntime() {
56
+ const packageNames = {
57
+ win32: "@rind-ai/runtime-win32-x64",
58
+ linux: "@rind-ai/runtime-linux-x64",
59
+ darwin: process.arch === "arm64" ? "@rind-ai/runtime-darwin-arm64" : "@rind-ai/runtime-darwin-x64",
60
+ };
61
+ const packageName = packageNames[process.platform];
62
+ if (!packageName) return "";
63
+ try {
64
+ const packageRoot = path.dirname(createRequire(import.meta.url).resolve(`${packageName}/package.json`));
65
+ return path.join(packageRoot, "bin", process.platform === "win32" ? "rind-runtime.exe" : "rind-runtime");
66
+ } catch {
67
+ return "";
68
+ }
69
+ }
70
+
71
+ if (cliArgs.some((arg) => arg === "--version" || arg === "--help" || arg === "-h")) {
72
+ process.exit(runHelpVersion({ python, repoRoot, runtimePath, cliArgs }));
73
+ }
74
+
75
+ let activeTurn = false;
76
+ let activeCompact = false;
77
+ let interruptRequested = false;
78
+ let input = null;
79
+ let inputActive = false;
80
+ let runtimeClosing = false;
81
+ let processExitTimer = null;
82
+ let cancelActiveInput = null;
83
+ let sessionInfo = {};
84
+ let latestStats = {};
85
+ let slashCommands = [];
86
+ let turnTools = { completed: 0, failed: 0 };
87
+ let pendingInputPrefill = "";
88
+ let assistantOutputLineOpen = false;
89
+ let assistantHeaderShown = false;
90
+ let outputStarted = false;
91
+ let promptPaused = false;
92
+ let activityFrame = 0;
93
+ let activityTimer = null;
94
+ let activityStartedAt = 0;
95
+ const assistantStreamBuffer = createAssistantStreamBuffer();
96
+ const assistantRenderer = new AssistantRenderer((text) => writeOutput(text));
97
+ const compactContextState = createCompactContextState();
98
+ const terminalUi = process.stdin.isTTY && process.stdout.isTTY
99
+ ? createTerminalUI({ input: process.stdin, output: process.stdout, render: renderActiveInput })
100
+ : null;
101
+ const promptEditor = createLineEditor();
102
+ let activeInputSession = null;
103
+ const runtimeClient = createRuntimeClient({
104
+ python,
105
+ repoRoot,
106
+ runtimePath,
107
+ cliArgs,
108
+ onMessage: (message) => void renderEvent(message).catch((error) => {
109
+ if (!runtimeClosing) {
110
+ writeErrorOutput(`${error instanceof Error ? error.message : String(error)}\n`);
111
+ }
112
+ }),
113
+ onStderr: (chunk) => writeErrorOutput(chunk),
114
+ onExit: (code, signal, { error }) => {
115
+ if (!runtimeClosing) {
116
+ writeErrorOutput(`Runtime stopped (${signal || (code ?? "startup failure")}): ${error.message}. Restart Rind to continue.\n`);
117
+ }
118
+ process.exitCode = runtimeClosing ? 0 : (code || 1);
119
+ closeInput();
120
+ if (runtimeClosing) {
121
+ scheduleProcessExit(process.exitCode ?? 0, 0);
122
+ }
123
+ },
124
+ });
125
+ const request = runtimeClient.request;
126
+ const turnState = {
127
+ get activeTurn() {
128
+ return activeTurn;
129
+ },
130
+ set activeTurn(value) {
131
+ activeTurn = Boolean(value);
132
+ },
133
+ get interruptRequested() {
134
+ return interruptRequested;
135
+ },
136
+ set interruptRequested(value) {
137
+ interruptRequested = Boolean(value);
138
+ },
139
+ get turnTools() {
140
+ return turnTools;
141
+ },
142
+ set turnTools(value) {
143
+ turnTools = value;
144
+ },
145
+ get runtimeClosing() {
146
+ return runtimeClosing;
147
+ },
148
+ };
149
+ const turnController = createTurnController({
150
+ request,
151
+ state: turnState,
152
+ refreshGoalState,
153
+ onTurnStart: () => {
154
+ assistantHeaderShown = false;
155
+ },
156
+ output: {
157
+ logQueuedInput: (text) => logOutput(queuedInputText(text)),
158
+ restoreInputText,
159
+ writeError: (text) => writeErrorOutput(`${text}\n`),
160
+ refreshInputState,
161
+ resetTurnTools,
162
+ closeAssistant,
163
+ cancelInput,
164
+ logInterrupt: () => logOutput(interruptText()),
165
+ },
166
+ });
167
+ const commandController = createCommandController({
168
+ request,
169
+ turn: turnController,
170
+ input: {
171
+ isTerminal: Boolean(terminalUi),
172
+ runGoalCommand,
173
+ runModelSelector,
174
+ startCompactCommand,
175
+ runSessionsSelector,
176
+ startReadonlySlashCommand,
177
+ },
178
+ state: {
179
+ get slashCommands() {
180
+ return slashCommands;
181
+ },
182
+ },
183
+ output: {
184
+ log: logOutput,
185
+ clearScreen: () => withSuspendedPrompt(() => console.clear()),
186
+ resetContextUsage,
187
+ setInputPrefill: (value) => {
188
+ pendingInputPrefill = value;
189
+ },
190
+ shutdown: shutdownRuntime,
191
+ exit: () => process.exit(0),
192
+ },
193
+ });
194
+ const backgroundController = createBackgroundController({
195
+ request,
196
+ terminalUi: Boolean(terminalUi),
197
+ state: {
198
+ get runtimeClosing() {
199
+ return runtimeClosing;
200
+ },
201
+ get sessionInfo() {
202
+ return sessionInfo;
203
+ },
204
+ set sessionInfo(value) {
205
+ sessionInfo = value;
206
+ },
207
+ get inputActive() {
208
+ return inputActive;
209
+ },
210
+ set inputActive(value) {
211
+ inputActive = Boolean(value);
212
+ },
213
+ },
214
+ redraw: redrawInput,
215
+ log: logOutput,
216
+ });
217
+ const eventController = createEventController({
218
+ state: {
219
+ get runtimeClosing() {
220
+ return runtimeClosing;
221
+ },
222
+ get activeTurn() {
223
+ return activeTurn;
224
+ },
225
+ debug: cliArgs.includes("--debug"),
226
+ },
227
+ input: { answerQuestion },
228
+ background: backgroundController,
229
+ output: {
230
+ assistantAppend: (text) => assistantRenderer.append(text),
231
+ handleContextBuilt: (event) => compactContextState.handleContextBuilt(event),
232
+ resetContextUsage,
233
+ closeAssistant,
234
+ log: logOutput,
235
+ debug: (text) => writeErrorOutput(`${text}\n`),
236
+ updateGoal: updateGoalState,
237
+ setStats: (stats) => {
238
+ latestStats = stats;
239
+ },
240
+ redraw: redrawInput,
241
+ clearCompactContext: () => compactContextState.clear(),
242
+ resetTurnTools,
243
+ },
244
+ });
245
+ const inputController = createInputController({
246
+ terminalUi,
247
+ state: {
248
+ get runtimeClosing() {
249
+ return runtimeClosing;
250
+ },
251
+ get promptPaused() {
252
+ return Boolean(promptPaused);
253
+ },
254
+ set promptPaused(value) {
255
+ promptPaused = Boolean(value);
256
+ },
257
+ },
258
+ askInput: ask,
259
+ onSubmit: (text) => turnController.submit(text),
260
+ onCommand: (text) => commandController.handle(text),
261
+ onSigint: handleSigint,
262
+ onPaste: handleTerminalPaste,
263
+ onInput: handleTerminalInput,
264
+ cancelInput,
265
+ renderPrompt: redrawInput,
266
+ prompt: mainPromptText,
267
+ placeholder: promptPlaceholderText,
268
+ });
269
+
270
+ process.on("SIGINT", handleSigint);
271
+
272
+ try {
273
+ const info = await request("initialize");
274
+ sessionInfo = { cwd: process.cwd(), ...(info || {}) };
275
+ slashCommands = commandController.normalizeCommands(info?.slash_commands);
276
+ if (terminalUi) {
277
+ inputController.start();
278
+ } else {
279
+ input = createInterface({
280
+ input: process.stdin,
281
+ output: process.stdout,
282
+ historySize: 100,
283
+ removeHistoryDuplicates: true,
284
+ });
285
+ process.stdin.on("data", handleStdinData);
286
+ }
287
+ if (terminalUi) {
288
+ void backgroundController.refresh().catch(() => {});
289
+ }
290
+ logOutput(startupText(info));
291
+ await inputController.promptLoop();
292
+ } catch (error) {
293
+ closeAssistant();
294
+ if (!isInputClosed(error)) {
295
+ writeErrorOutput(`${error instanceof Error ? error.message : String(error)}\n`);
296
+ process.exitCode = 1;
297
+ }
298
+ } finally {
299
+ closeRuntime();
300
+ }
301
+
302
+ async function runGoalCommand(command) {
303
+ if (command.action === "set" && turnState.activeTurn) {
304
+ logOutput(commandResultText("Goal not started", "pause or finish the active turn first"));
305
+ return;
306
+ }
307
+ try {
308
+ if (command.action === "set") {
309
+ const result = await request("goal.set", { objective: command.objective });
310
+ updateGoalState(result?.goal);
311
+ logOutput(goalCommandText(result?.goal, "set"));
312
+ turnController.submit(command.objective);
313
+ return;
314
+ }
315
+ if (command.action === "clear") {
316
+ const result = await request("goal.clear");
317
+ updateGoalState(result?.goal || null);
318
+ logOutput(goalCommandText(null, "clear"));
319
+ return;
320
+ }
321
+ if (command.action === "pause" || command.action === "resume") {
322
+ const result = await request("goal.status", { status: command.action === "resume" ? "active" : "paused" });
323
+ updateGoalState(result?.goal);
324
+ logOutput(goalCommandText(result?.goal, command.action));
325
+ if (command.action === "resume" && !turnState.activeTurn) {
326
+ turnController.submit("", { goal_continuation: true });
327
+ }
328
+ return;
329
+ }
330
+ const result = await request("goal.get");
331
+ updateGoalState(result?.goal || null);
332
+ logOutput(goalCommandText(result?.goal || null));
333
+ } catch (error) {
334
+ logOutput(`Goal command failed: ${error instanceof Error ? error.message : String(error)}`);
335
+ }
336
+ }
337
+
338
+ function updateGoalState(goal) {
339
+ sessionInfo = { ...sessionInfo, goal: goal && typeof goal === "object" ? goal : null };
340
+ redrawInput();
341
+ }
342
+
343
+ async function refreshGoalState() {
344
+ if (!Array.isArray(sessionInfo.capabilities) || !sessionInfo.capabilities.includes("goals")) {
345
+ return;
346
+ }
347
+ try {
348
+ const result = await request("goal.get");
349
+ updateGoalState(result?.goal || null);
350
+ } catch {
351
+ // The turn result remains usable when a late state refresh races shutdown.
352
+ }
353
+ }
354
+
355
+ async function runSessionsSelector() {
356
+ let result;
357
+ try {
358
+ result = await request("slash.execute", { input: "/sessions" });
359
+ } catch (error) {
360
+ logOutput(`Command failed: ${error instanceof Error ? error.message : String(error)}`);
361
+ return;
362
+ }
363
+ const sessions = Array.isArray(result?.display?.sessions) ? result.display.sessions : [];
364
+ if (!sessions.length) {
365
+ await commandController.applyResult(result);
366
+ return;
367
+ }
368
+ const currentId = String(result?.display?.current_session_id || sessionInfo.session_id || "");
369
+ const options = sessions.map((session) => sessionMenuOption(session));
370
+ const currentIndex = sessions.findIndex((session) => String(session?.id || "") === currentId);
371
+ const selected = await askSessionMenu(options, sessions, currentIndex);
372
+ if (!selected || runtimeClosing || selected.id === currentId) {
373
+ return;
374
+ }
375
+ try {
376
+ const update = await request("session.switch", { session_id: selected.id });
377
+ backgroundController.clear();
378
+ sessionInfo = {
379
+ ...sessionInfo,
380
+ session_id: update?.session_id || selected.id,
381
+ model: update?.model || sessionInfo.model,
382
+ resume_preview: update?.resume_preview || "",
383
+ goal: update?.goal || null,
384
+ background_count: 0,
385
+ };
386
+ latestStats = update?.usage && typeof update.usage === "object" ? update.usage : {};
387
+ compactContextState.clear();
388
+ logOutput(sessionSwitchedText(sessionInfo));
389
+ redrawInput();
390
+ void backgroundController.refresh().catch(() => {});
391
+ } catch (error) {
392
+ logOutput(`Session switch failed: ${error instanceof Error ? error.message : String(error)}`);
393
+ }
394
+ }
395
+
396
+ function sessionMenuOption(session) {
397
+ const id = singleLineText(session?.id) || "unknown";
398
+ const title = singleLineText(session?.title);
399
+ const updated = singleLineText(session?.updated_at);
400
+ const marker = session?.current ? "current" : "";
401
+ return [id, title, updated, marker].filter(Boolean).join(" · ");
402
+ }
403
+
404
+ function startReadonlySlashCommand(text) {
405
+ void runReadonlySlashCommand(text).catch((error) => {
406
+ if (!runtimeClosing) {
407
+ writeErrorOutput(`${error instanceof Error ? error.message : String(error)}\n`);
408
+ }
409
+ });
410
+ }
411
+
412
+ async function runReadonlySlashCommand(text) {
413
+ const result = await request("slash.execute", { input: text });
414
+ await commandController.applyResult(result);
415
+ }
416
+
417
+ function startCompactCommand() {
418
+ if (activeCompact) {
419
+ logOutput("Compact is already running.");
420
+ return;
421
+ }
422
+ activeCompact = true;
423
+ interruptRequested = false;
424
+ refreshInputState();
425
+ void runCompactCommand().catch((error) => {
426
+ if (!runtimeClosing) {
427
+ writeErrorOutput(`${error instanceof Error ? error.message : String(error)}\n`);
428
+ }
429
+ });
430
+ }
431
+
432
+ async function runCompactCommand() {
433
+ try {
434
+ const result = await request("slash.execute", { input: "/compact" });
435
+ await commandController.applyResult(result);
436
+ } finally {
437
+ activeCompact = false;
438
+ interruptRequested = false;
439
+ refreshInputState();
440
+ }
441
+ }
442
+
443
+ async function runModelSelector() {
444
+ let result;
445
+ try {
446
+ result = await request("models.list");
447
+ } catch (error) {
448
+ logOutput(modelListErrorText(error instanceof Error ? error.message : String(error), sessionInfo.model));
449
+ return;
450
+ }
451
+
452
+ const currentModel = result?.current_model || sessionInfo.model || result?.default_model || "";
453
+ const selected = await askModelMenu(result?.models || [], currentModel);
454
+ if (!selected || runtimeClosing) {
455
+ return;
456
+ }
457
+
458
+ try {
459
+ const update = await request("model.set", { model: selected });
460
+ sessionInfo = { ...sessionInfo, model: update?.model || selected };
461
+ logOutput(modelSetResultText(update, selected));
462
+ } catch (error) {
463
+ logOutput(`Command failed: ${error instanceof Error ? error.message : String(error)}`);
464
+ }
465
+ }
466
+
467
+ function modelSetResultText(result, model) {
468
+ const sessionModel = singleLineText(result?.session_model || result?.model || model);
469
+ const defaultModel = singleLineText(result?.default_model);
470
+ const lines = ["Session model updated."];
471
+ if (sessionModel) {
472
+ lines.push(`- session model: ${sessionModel}`);
473
+ }
474
+ if (defaultModel) {
475
+ lines.push(`- default model: ${defaultModel} (unchanged)`);
476
+ }
477
+ if (result?.active_updated || result?.runtime || result?.session) {
478
+ lines.push("- active session: updated");
479
+ } else {
480
+ lines.push("- active session: unchanged; start a new session to use this model");
481
+ }
482
+ return commandResultText(lines[0], lines.slice(1).join(" · "));
483
+ }
484
+
485
+ function restoreInputText(text) {
486
+ pendingInputPrefill = String(text || "");
487
+ if (activeInputSession?.editor) {
488
+ activeInputSession.editor.setInput(pendingInputPrefill);
489
+ pendingInputPrefill = "";
490
+ redrawInput();
491
+ }
492
+ }
493
+
494
+ function inputState() {
495
+ const running = activeTurn || activeCompact;
496
+ return {
497
+ running,
498
+ label: activeCompact ? "Compacting" : "Working",
499
+ frame: activityFrame,
500
+ elapsedMs: running ? Date.now() - activityStartedAt : 0,
501
+ };
502
+ }
503
+
504
+ function mainPromptText() {
505
+ return promptText(sessionInfo, latestStats, inputState());
506
+ }
507
+
508
+ function refreshInputState() {
509
+ updateActivityTimer();
510
+ redrawInput();
511
+ }
512
+
513
+ function resetContextUsage() {
514
+ latestStats = { context_usage_percent: 0 };
515
+ redrawInput();
516
+ }
517
+
518
+ function redrawInput(force = false) {
519
+ if (!inputActive || runtimeClosing || !terminalUi) {
520
+ return;
521
+ }
522
+ terminalUi.requestRender(force);
523
+ }
524
+
525
+ function updateActivityTimer() {
526
+ if (activeTurn || activeCompact) {
527
+ if (!activityStartedAt) {
528
+ activityStartedAt = Date.now();
529
+ }
530
+ if (activityTimer) {
531
+ return;
532
+ }
533
+ activityTimer = setInterval(() => {
534
+ activityFrame += 1;
535
+ redrawInput();
536
+ }, 300);
537
+ activityTimer.unref?.();
538
+ return;
539
+ }
540
+ clearActivityTimer();
541
+ }
542
+
543
+ function clearActivityTimer() {
544
+ if (!activityTimer) {
545
+ return;
546
+ }
547
+ clearInterval(activityTimer);
548
+ activityTimer = null;
549
+ activityFrame = 0;
550
+ activityStartedAt = 0;
551
+ }
552
+
553
+ function logOutput(text) {
554
+ flushAssistantText(assistantStreamBuffer.flush(), { redraw: true });
555
+ withSuspendedPrompt(() => {
556
+ closeOpenAssistantOutputLine();
557
+ process.stdout.write(outputBlockText(text, outputStarted));
558
+ outputStarted = true;
559
+ });
560
+ }
561
+
562
+ function writeOutput(text) {
563
+ const holdPartialLine = inputActive && Boolean(terminalUi);
564
+ flushAssistantText(assistantStreamBuffer.push(text, holdPartialLine));
565
+ }
566
+
567
+ function flushAssistantText(text = "", options = {}) {
568
+ const output = String(text || "");
569
+ if (!output) {
570
+ return;
571
+ }
572
+ withSuspendedPrompt(() => {
573
+ writeAssistantHeader();
574
+ process.stdout.write(output);
575
+ assistantOutputLineOpen = output ? !output.endsWith("\n") : assistantOutputLineOpen;
576
+ }, { redraw: options.redraw !== false });
577
+ }
578
+
579
+ function writeUserInput(text) {
580
+ const line = userInputText(text);
581
+ if (!line) {
582
+ return;
583
+ }
584
+ flushAssistantText(assistantStreamBuffer.flush(), { redraw: false });
585
+ closeOpenAssistantOutputLine();
586
+ process.stdout.write(outputBlockText(line, outputStarted));
587
+ outputStarted = true;
588
+ }
589
+
590
+ function writeErrorOutput(text) {
591
+ flushAssistantText(assistantStreamBuffer.flush(), { redraw: true });
592
+ withSuspendedPrompt(() => process.stderr.write(String(text || "")));
593
+ }
594
+
595
+ function withSuspendedPrompt(action, options = {}) {
596
+ if (!terminalUi || !inputActive || runtimeClosing) {
597
+ action();
598
+ return;
599
+ }
600
+ terminalUi.withSuspended(action, { render: options.redraw !== false });
601
+ }
602
+
603
+ function closeOpenAssistantOutputLine() {
604
+ if (!assistantOutputLineOpen) {
605
+ return;
606
+ }
607
+ process.stdout.write("\n");
608
+ assistantOutputLineOpen = false;
609
+ }
610
+
611
+ function writeAssistantHeader() {
612
+ if (assistantHeaderShown) {
613
+ return;
614
+ }
615
+ process.stdout.write(outputBlockText(assistantHeaderText(), outputStarted));
616
+ outputStarted = true;
617
+ assistantHeaderShown = true;
618
+ }
619
+
620
+ async function renderEvent(message) {
621
+ return eventController.handle(message);
622
+ }
623
+
624
+ function resetTurnTools() {
625
+ turnTools = { completed: 0, failed: 0 };
626
+ }
627
+
628
+ async function answerQuestion(event) {
629
+ pausePrompt();
630
+ closeAssistant();
631
+ logOutput(questionText(event));
632
+ try {
633
+ const options = Array.isArray(event.options) ? event.options : [];
634
+ const answer = terminalUi && options.length
635
+ ? await askChoiceMenu(event)
636
+ : selectAnswer((await ask(answerPromptText(), answerPlaceholderText())).trim(), options);
637
+ if (interruptRequested || runtimeClosing) {
638
+ return;
639
+ }
640
+ await request("user_question.respond", {
641
+ tool_call_id: event.tool_call_id,
642
+ answer,
643
+ });
644
+ } finally {
645
+ resumePrompt();
646
+ }
647
+ }
648
+
649
+ function selectAnswer(raw, options) {
650
+ const index = Number(raw);
651
+ if (Number.isInteger(index) && index >= 1 && index <= options.length) {
652
+ return options[index - 1];
653
+ }
654
+ return raw;
655
+ }
656
+
657
+ function ask(prompt, placeholder = "") {
658
+ if (!terminalUi) {
659
+ if (!input) {
660
+ return Promise.reject(new Error("Input is not available"));
661
+ }
662
+ return askLine(promptValue(prompt));
663
+ }
664
+ return askTtyInput(prompt, placeholder);
665
+ }
666
+
667
+ function askLine(prompt) {
668
+ return new Promise((resolve, reject) => {
669
+ const cleanup = () => {
670
+ inputActive = false;
671
+ input.off("close", onClose);
672
+ if (cancelActiveInput === onCancel) {
673
+ cancelActiveInput = null;
674
+ }
675
+ };
676
+ const onClose = () => {
677
+ cleanup();
678
+ reject(new Error("Input closed"));
679
+ };
680
+ const onCancel = () => {
681
+ cleanup();
682
+ resolve("");
683
+ };
684
+ input.once("close", onClose);
685
+ cancelActiveInput = onCancel;
686
+ inputActive = true;
687
+ input.question(prompt, (answer) => {
688
+ cleanup();
689
+ resolve(answer);
690
+ });
691
+ applyInputPrefill();
692
+ });
693
+ }
694
+
695
+ function askTtyInput(prompt, placeholder) {
696
+ return new Promise((resolve) => {
697
+ clearAssistantLineForInput();
698
+ const mode = placeholder && placeholder !== answerPlaceholderText() ? "prompt" : "line";
699
+ const initialInput = pendingInputPrefill;
700
+ const editor = mode === "prompt" ? promptEditor : createLineEditor(initialInput);
701
+ if (mode === "prompt") {
702
+ editor.setInput(initialInput);
703
+ }
704
+ editor.setViewportWidth(process.stdout.columns || 80);
705
+ const menuState = mode === "prompt" ? createSlashMenuState(slashCommands) : null;
706
+ pendingInputPrefill = "";
707
+ const session = { mode, prompt, placeholder, editor, menuState, resolve };
708
+ activeInputSession = session;
709
+ inputActive = true;
710
+ cancelActiveInput = () => completeTtyInput(session, "", false);
711
+ redrawInput(true);
712
+ });
713
+ }
714
+
715
+ function clearAssistantLineForInput() {
716
+ if (!assistantOutputLineOpen) {
717
+ return;
718
+ }
719
+ if (terminalUi) {
720
+ terminalUi.withSuspended(closeOpenAssistantOutputLine, { render: false });
721
+ } else {
722
+ closeOpenAssistantOutputLine();
723
+ }
724
+ }
725
+
726
+ function renderActiveInput(width = process.stdout.columns || 80) {
727
+ if (backgroundController.isMonitoring()) {
728
+ return backgroundController.frame(width);
729
+ }
730
+ const session = activeInputSession;
731
+ if (!session) {
732
+ return { lines: [], cursorRow: 0, cursorColumn: 0 };
733
+ }
734
+ if (session.mode === "model") {
735
+ return prepareComposerFrame({
736
+ prompt: mainPromptText(),
737
+ inputText: session.inputText,
738
+ cursor: { line: 0, column: session.inputText.length },
739
+ menuText: modelMenuText(session.modelState.items(), session.modelState.selectedIndex()).trimEnd(),
740
+ }, width);
741
+ }
742
+ if (session.mode === "choice") {
743
+ return prepareComposerFrame({
744
+ prompt: mainPromptText(),
745
+ inputText: session.question,
746
+ cursor: { line: 0, column: session.question.length },
747
+ menuText: choiceMenuText(session.choiceState.options(), session.choiceState.selectedIndex(), session.recommended).trimEnd(),
748
+ }, width);
749
+ }
750
+ if (session.mode === "sessions") {
751
+ return prepareComposerFrame({
752
+ prompt: mainPromptText(),
753
+ inputText: session.inputText,
754
+ cursor: { line: 0, column: session.inputText.length },
755
+ menuText: sessionMenuText(session.choiceState.options(), session.choiceState.selectedIndex()).trimEnd(),
756
+ }, width);
757
+ }
758
+ session.editor.setViewportWidth(width);
759
+ const matches = session.menuState ? syncSlashMenu(session) : [];
760
+ return prepareComposerFrame({
761
+ prompt: promptValue(session.prompt),
762
+ inputText: session.editor.input(),
763
+ cursor: session.editor.cursorPosition(),
764
+ placeholder: session.mode === "prompt" ? inputHintText(session.placeholder) : "",
765
+ menuText: session.menuState ? slashMenuText(matches, session.menuState.selectedIndex()).trimEnd() : "",
766
+ }, width);
767
+ }
768
+
769
+ function syncSlashMenu(session) {
770
+ session.menuState.setInput(session.editor.input());
771
+ return session.menuState.matches();
772
+ }
773
+
774
+ function handleTerminalInput(raw = "") {
775
+ const event = parseTerminalKey(raw);
776
+ if (!event) {
777
+ return;
778
+ }
779
+ if (event.ctrl && event.name === "c") {
780
+ handleSigint();
781
+ return;
782
+ }
783
+ if (backgroundController.isMonitoring()) {
784
+ backgroundController.handleInput(event);
785
+ return;
786
+ }
787
+ if (event.ctrl && event.name === "b") {
788
+ backgroundController.enterMonitor();
789
+ return;
790
+ }
791
+ const session = activeInputSession;
792
+ if (!session) {
793
+ return;
794
+ }
795
+ if (session.mode === "model") {
796
+ handleModelInput(session, event);
797
+ return;
798
+ }
799
+ if (session.mode === "choice") {
800
+ handleChoiceInput(session, event);
801
+ return;
802
+ }
803
+ if (session.mode === "sessions") {
804
+ handleSessionInput(session, event);
805
+ return;
806
+ }
807
+ const key = event;
808
+ const matches = session.menuState ? syncSlashMenu(session) : [];
809
+ const menuKey = !key.ctrl && !key.alt && !key.shift && ["escape", "up", "down"].includes(key.name);
810
+ if (session.menuState && matches.length && menuKey) {
811
+ if (session.menuState.handleKey("", key)) {
812
+ redrawInput();
813
+ return;
814
+ }
815
+ }
816
+ const result = session.editor.handleInput(key);
817
+ if (result === "submit") {
818
+ submitTtyInput(session);
819
+ return;
820
+ }
821
+ if (result) {
822
+ redrawInput();
823
+ }
824
+ }
825
+
826
+ function handleTerminalPaste(text) {
827
+ if (backgroundController.isMonitoring()) {
828
+ return;
829
+ }
830
+ const session = activeInputSession;
831
+ if (!session || session.mode === "model" || session.mode === "choice" || session.mode === "sessions") {
832
+ return;
833
+ }
834
+ session.editor.handleInput({ kind: "paste", text });
835
+ redrawInput();
836
+ }
837
+
838
+ function handleModelInput(session, key) {
839
+ const modified = key.ctrl || key.alt || key.shift;
840
+ if (!modified && (key.name === "enter" || key.name === "return")) {
841
+ const model = session.modelState.selectedModel()?.name || "";
842
+ completeTtyInput(session, model, Boolean(model), "", model ? `/model set ${model}` : "");
843
+ return;
844
+ }
845
+ if (!modified && key.name === "escape") {
846
+ completeTtyInput(session, "", false);
847
+ return;
848
+ }
849
+ if (!modified && session.modelState.handleKey(key)) {
850
+ redrawInput();
851
+ }
852
+ }
853
+
854
+ function submitTtyInput(session) {
855
+ if (session.menuState) {
856
+ syncSlashMenu(session);
857
+ }
858
+ const command = session.menuState?.selectedCommand();
859
+ const value = command ? `/${command.name}` : session.editor.input();
860
+ if (session.mode === "prompt") {
861
+ session.editor.addToHistory(value);
862
+ }
863
+ completeTtyInput(session, value, session.mode === "prompt", session.mode === "line" ? "\n" : "");
864
+ }
865
+
866
+ function completeTtyInput(session, value, writeUser, lineText = "", displayValue = value) {
867
+ if (activeInputSession !== session) {
868
+ return;
869
+ }
870
+ activeInputSession = null;
871
+ inputActive = false;
872
+ if (cancelActiveInput) {
873
+ cancelActiveInput = null;
874
+ }
875
+ const writeAction = () => {
876
+ if (writeUser) {
877
+ writeUserInput(displayValue);
878
+ } else if (lineText && String(value || "").trim()) {
879
+ process.stdout.write("\n");
880
+ }
881
+ };
882
+ if (terminalUi) {
883
+ terminalUi.withSuspended(writeAction, { render: false });
884
+ } else {
885
+ writeAction();
886
+ }
887
+ session.resolve(value);
888
+ }
889
+
890
+ function askModelMenu(models, currentModel) {
891
+ return new Promise((resolve) => {
892
+ clearAssistantLineForInput();
893
+ const state = createModelMenuState(models, currentModel);
894
+ if (!state.items().length) {
895
+ resolve("");
896
+ return;
897
+ }
898
+ const session = {
899
+ mode: "model",
900
+ inputText: "/model",
901
+ modelState: state,
902
+ resolve,
903
+ };
904
+ activeInputSession = session;
905
+ inputActive = true;
906
+ cancelActiveInput = () => completeTtyInput(session, "", false);
907
+ redrawInput(true);
908
+ });
909
+ }
910
+
911
+ function askChoiceMenu(event) {
912
+ return new Promise((resolve) => {
913
+ clearAssistantLineForInput();
914
+ const state = createChoiceMenuState(event.options, event.recommended);
915
+ const session = {
916
+ mode: "choice",
917
+ question: String(event.question || "Input required"),
918
+ choiceState: state,
919
+ recommended: event.recommended || "",
920
+ resolve,
921
+ };
922
+ activeInputSession = session;
923
+ inputActive = true;
924
+ cancelActiveInput = () => completeTtyInput(session, "", false);
925
+ redrawInput(true);
926
+ });
927
+ }
928
+
929
+ function askSessionMenu(options, sessions, currentIndex) {
930
+ return new Promise((resolve) => {
931
+ clearAssistantLineForInput();
932
+ const state = createChoiceMenuState(options, options[currentIndex] || "");
933
+ const session = {
934
+ mode: "sessions",
935
+ inputText: "/sessions",
936
+ choiceState: state,
937
+ sessions,
938
+ resolve,
939
+ };
940
+ activeInputSession = session;
941
+ inputActive = true;
942
+ cancelActiveInput = () => completeTtyInput(session, null, false);
943
+ redrawInput(true);
944
+ });
945
+ }
946
+
947
+ function handleChoiceInput(session, key) {
948
+ const modified = key.ctrl || key.alt || key.shift;
949
+ if (!modified && (key.name === "enter" || key.name === "return")) {
950
+ completeTtyInput(session, session.choiceState.selectedOption(), false);
951
+ return;
952
+ }
953
+ if (!modified && key.name === "escape") {
954
+ completeTtyInput(session, "", false);
955
+ return;
956
+ }
957
+ if (!modified && session.choiceState.handleKey(key)) {
958
+ redrawInput();
959
+ }
960
+ }
961
+
962
+ function handleSessionInput(session, key) {
963
+ const modified = key.ctrl || key.alt || key.shift;
964
+ if (!modified && (key.name === "enter" || key.name === "return")) {
965
+ const index = session.choiceState.selectedIndex();
966
+ completeTtyInput(session, session.sessions[index] || null, false);
967
+ return;
968
+ }
969
+ if (!modified && key.name === "escape") {
970
+ completeTtyInput(session, null, false);
971
+ return;
972
+ }
973
+ if (!modified && session.choiceState.handleKey(key)) {
974
+ redrawInput();
975
+ }
976
+ }
977
+
978
+ function singleLineText(value) {
979
+ return String(value || "").replace(/\s+/g, " ").trim();
980
+ }
981
+
982
+ function pausePrompt() {
983
+ inputController.pause();
984
+ }
985
+
986
+ function resumePrompt() {
987
+ inputController.resume();
988
+ }
989
+
990
+ function applyInputPrefill() {
991
+ if (!pendingInputPrefill) {
992
+ return;
993
+ }
994
+ const text = pendingInputPrefill;
995
+ pendingInputPrefill = "";
996
+ input.write(text);
997
+ }
998
+
999
+ function promptValue(prompt) {
1000
+ return typeof prompt === "function" ? prompt() : prompt;
1001
+ }
1002
+
1003
+
1004
+
1005
+ function handleSigint() {
1006
+ const action = sigintAction({ activeTurn: activeTurn || activeCompact, interruptRequested, runtimeClosing });
1007
+ if (action === "interrupt") {
1008
+ interruptTurn();
1009
+ } else {
1010
+ exitFromSignal();
1011
+ }
1012
+ }
1013
+
1014
+ function interruptTurn() {
1015
+ turnController.interrupt();
1016
+ }
1017
+
1018
+ function handleStdinData(chunk) {
1019
+ if (Buffer.from(chunk).includes(3)) {
1020
+ handleSigint();
1021
+ }
1022
+ }
1023
+
1024
+ function exitFromSignal() {
1025
+ if (runtimeClosing) {
1026
+ forceCloseRuntime();
1027
+ scheduleProcessExit(0, 0);
1028
+ return;
1029
+ }
1030
+ void shutdownRuntime();
1031
+ }
1032
+
1033
+ function closeAssistant() {
1034
+ assistantRenderer.finish();
1035
+ flushAssistantText(assistantStreamBuffer.flush());
1036
+ assistantHeaderShown = false;
1037
+ }
1038
+
1039
+ function closeRuntime() {
1040
+ if (runtimeClosing) {
1041
+ return;
1042
+ }
1043
+ runtimeClosing = true;
1044
+ clearActivityTimer();
1045
+ backgroundController.stop();
1046
+ void runtimeClient.shutdown();
1047
+ closeInput();
1048
+ }
1049
+
1050
+ function forceCloseRuntime() {
1051
+ runtimeClosing = true;
1052
+ clearActivityTimer();
1053
+ backgroundController.stop();
1054
+ closeInput();
1055
+ runtimeClient.forceShutdown();
1056
+ }
1057
+
1058
+ function cancelInput() {
1059
+ const cancel = cancelActiveInput;
1060
+ cancelActiveInput = null;
1061
+ cancel?.();
1062
+ }
1063
+
1064
+ async function shutdownRuntime() {
1065
+ if (runtimeClosing) {
1066
+ return;
1067
+ }
1068
+ runtimeClosing = true;
1069
+ clearActivityTimer();
1070
+ backgroundController.stop();
1071
+ try {
1072
+ await runtimeClient.shutdown();
1073
+ } catch {
1074
+ runtimeClient.forceShutdown();
1075
+ } finally {
1076
+ runtimeClient.closeInput();
1077
+ closeInput();
1078
+ }
1079
+ }
1080
+
1081
+ function scheduleProcessExit(code, delayMs) {
1082
+ if (processExitTimer) {
1083
+ return;
1084
+ }
1085
+ process.exitCode = code;
1086
+ processExitTimer = setTimeout(() => {
1087
+ try {
1088
+ closeInput();
1089
+ } finally {
1090
+ process.exit(code);
1091
+ }
1092
+ }, delayMs);
1093
+ }
1094
+
1095
+ function closeInput() {
1096
+ inputController.close();
1097
+ process.stdin.off("data", handleStdinData);
1098
+ if (input) {
1099
+ const current = input;
1100
+ input = null;
1101
+ try {
1102
+ current.close();
1103
+ } catch {
1104
+ // Ignore readline close races during signal shutdown.
1105
+ }
1106
+ }
1107
+ if (!terminalUi) {
1108
+ process.stdin.pause();
1109
+ }
1110
+ }
1111
+ }