@ouro.bot/cli 0.1.0-alpha.175 → 0.1.0-alpha.177
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.json +7 -1
- package/dist/heart/active-work.js +2 -2
- package/dist/heart/core.js +1 -1
- package/dist/mind/file-state.js +179 -0
- package/dist/mind/prompt.js +119 -60
- package/dist/mind/scrutiny.js +173 -0
- package/dist/nerves/coverage/audit.js +1 -1
- package/dist/repertoire/ado-semantic.js +5 -5
- package/dist/repertoire/coding/tools.js +35 -5
- package/dist/repertoire/shell-sessions.js +133 -0
- package/dist/repertoire/tools-base.js +145 -16
- package/dist/repertoire/tools.js +37 -0
- package/dist/senses/cli/adapter.js +307 -0
- package/dist/senses/cli/ink-app.js +156 -0
- package/dist/senses/cli/inline-diff.js +68 -0
- package/dist/senses/cli/message-list.js +84 -0
- package/dist/senses/cli/ouro-tui.js +296 -0
- package/dist/senses/cli/spinner-imperative.js +135 -0
- package/dist/senses/cli/spinner.js +101 -0
- package/dist/senses/cli/status-line.js +64 -0
- package/dist/senses/cli/streaming-markdown.js +486 -0
- package/dist/senses/cli/tool-display.js +77 -0
- package/dist/senses/cli/tool-render.js +89 -0
- package/dist/senses/cli/tui-store.js +198 -0
- package/dist/senses/cli/virtual-list.js +68 -0
- package/dist/senses/cli.js +231 -157
- package/dist/senses/pipeline.js +53 -10
- package/package.json +3 -1
- package/dist/outlook-ui/assets/index-DN6vVYPm.css +0 -1
- package/dist/outlook-ui/assets/index-M1l1tDTX.js +0 -61
- package/dist/outlook-ui/index.html +0 -15
package/dist/senses/cli.js
CHANGED
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.MarkdownStreamer = exports.InputController = exports.Spinner = exports.StreamingWordWrapper = exports.wrapCliText = exports.formatEchoedInputSummary = void 0;
|
|
36
|
+
exports.InputQueue = exports.MarkdownStreamer = exports.InputController = exports.Spinner = exports.StreamingWordWrapper = exports.wrapCliText = exports.formatEchoedInputSummary = void 0;
|
|
37
37
|
exports.formatPendingPrefix = formatPendingPrefix;
|
|
38
38
|
exports.getCliContinuityIngressTexts = getCliContinuityIngressTexts;
|
|
39
39
|
exports.writeCliAsyncAssistantMessage = writeCliAsyncAssistantMessage;
|
|
@@ -75,6 +75,8 @@ const bundle_meta_1 = require("../heart/daemon/hooks/bundle-meta");
|
|
|
75
75
|
const agent_config_v2_1 = require("../heart/daemon/hooks/agent-config-v2");
|
|
76
76
|
const bundle_manifest_1 = require("../mind/bundle-manifest");
|
|
77
77
|
const cli_layout_1 = require("./cli-layout");
|
|
78
|
+
const spinner_imperative_1 = require("./cli/spinner-imperative");
|
|
79
|
+
const tool_display_1 = require("./cli/tool-display");
|
|
78
80
|
var cli_layout_2 = require("./cli-layout");
|
|
79
81
|
Object.defineProperty(exports, "formatEchoedInputSummary", { enumerable: true, get: function () { return cli_layout_2.formatEchoedInputSummary; } });
|
|
80
82
|
Object.defineProperty(exports, "wrapCliText", { enumerable: true, get: function () { return cli_layout_2.wrapCliText; } });
|
|
@@ -96,7 +98,7 @@ function getCliContinuityIngressTexts(input) {
|
|
|
96
98
|
const trimmed = input.trim();
|
|
97
99
|
return trimmed ? [trimmed] : [];
|
|
98
100
|
}
|
|
99
|
-
const CLI_PROMPT = "\x1b[36m
|
|
101
|
+
const CLI_PROMPT = "\x1b[36m) \x1b[0m";
|
|
100
102
|
function writeCliAsyncAssistantMessage(rl, message, stdout = process.stdout) {
|
|
101
103
|
const rlInt = rl;
|
|
102
104
|
const currentLine = rlInt.line ?? "";
|
|
@@ -119,82 +121,9 @@ function pauseActiveSpinner() { _activeSpinner?.pause(); }
|
|
|
119
121
|
function resumeActiveSpinner() { _activeSpinner?.resume(); }
|
|
120
122
|
/* v8 ignore stop */
|
|
121
123
|
function setActiveSpinner(s) { _activeSpinner = s; }
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
126
|
-
i = 0;
|
|
127
|
-
iv = null;
|
|
128
|
-
piv = null;
|
|
129
|
-
msg = "";
|
|
130
|
-
phrases = null;
|
|
131
|
-
lastPhrase = "";
|
|
132
|
-
stopped = false;
|
|
133
|
-
constructor(m = "working", phrases) {
|
|
134
|
-
this.msg = m;
|
|
135
|
-
if (phrases && phrases.length > 0)
|
|
136
|
-
this.phrases = phrases;
|
|
137
|
-
}
|
|
138
|
-
start() {
|
|
139
|
-
this.stopped = false;
|
|
140
|
-
process.stderr.write("\r\x1b[K");
|
|
141
|
-
this.spin();
|
|
142
|
-
this.iv = setInterval(() => this.spin(), 80);
|
|
143
|
-
if (this.phrases) {
|
|
144
|
-
this.piv = setInterval(() => this.rotatePhrase(), 1500);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
spin() {
|
|
148
|
-
// Guard: clearInterval can't prevent already-dequeued callbacks
|
|
149
|
-
/* v8 ignore next -- race guard: timer callback fires after stop() @preserve */
|
|
150
|
-
if (this.stopped)
|
|
151
|
-
return;
|
|
152
|
-
process.stderr.write(`\r\x1b[K${this.frames[this.i]} ${this.msg}... `);
|
|
153
|
-
this.i = (this.i + 1) % this.frames.length;
|
|
154
|
-
}
|
|
155
|
-
rotatePhrase() {
|
|
156
|
-
/* v8 ignore next -- race guard: timer callback fires after stop() @preserve */
|
|
157
|
-
if (this.stopped)
|
|
158
|
-
return;
|
|
159
|
-
const next = (0, phrases_1.pickPhrase)(this.phrases, this.lastPhrase);
|
|
160
|
-
this.lastPhrase = next;
|
|
161
|
-
this.msg = next;
|
|
162
|
-
}
|
|
163
|
-
/* v8 ignore start -- pause/resume: exercised at runtime via log sink coordination @preserve */
|
|
164
|
-
/** Clear the spinner line temporarily so other output can print cleanly. */
|
|
165
|
-
pause() {
|
|
166
|
-
if (this.stopped)
|
|
167
|
-
return;
|
|
168
|
-
process.stderr.write("\r\x1b[K");
|
|
169
|
-
}
|
|
170
|
-
/** Restore the spinner line after a pause. */
|
|
171
|
-
resume() {
|
|
172
|
-
if (this.stopped)
|
|
173
|
-
return;
|
|
174
|
-
this.spin();
|
|
175
|
-
}
|
|
176
|
-
/* v8 ignore stop */
|
|
177
|
-
stop(ok) {
|
|
178
|
-
this.stopped = true;
|
|
179
|
-
if (this.iv) {
|
|
180
|
-
clearInterval(this.iv);
|
|
181
|
-
this.iv = null;
|
|
182
|
-
}
|
|
183
|
-
if (this.piv) {
|
|
184
|
-
clearInterval(this.piv);
|
|
185
|
-
this.piv = null;
|
|
186
|
-
}
|
|
187
|
-
process.stderr.write("\r\x1b[K");
|
|
188
|
-
/* v8 ignore next -- ok parameter currently unused by callers @preserve */
|
|
189
|
-
if (ok)
|
|
190
|
-
process.stderr.write(`\x1b[32m\u2713\x1b[0m ${ok}\n`);
|
|
191
|
-
}
|
|
192
|
-
fail(msg) {
|
|
193
|
-
this.stop();
|
|
194
|
-
process.stderr.write(`\x1b[31m\u2717\x1b[0m ${msg}\n`);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
exports.Spinner = Spinner;
|
|
124
|
+
// Re-export ImperativeSpinner as Spinner for backward compatibility
|
|
125
|
+
var spinner_imperative_2 = require("./cli/spinner-imperative");
|
|
126
|
+
Object.defineProperty(exports, "Spinner", { enumerable: true, get: function () { return spinner_imperative_2.ImperativeSpinner; } });
|
|
198
127
|
// Input controller: pauses readline during model/tool execution.
|
|
199
128
|
// Does NOT touch raw mode — readline with terminal:true manages raw mode
|
|
200
129
|
// internally. Touching it causes ^C to be echoed by the terminal driver.
|
|
@@ -377,7 +306,7 @@ function createCliCallbacks() {
|
|
|
377
306
|
const phrases = (0, phrases_1.getPhrases)();
|
|
378
307
|
const pool = hadToolRun ? phrases.followup : phrases.thinking;
|
|
379
308
|
const first = (0, phrases_1.pickPhrase)(pool);
|
|
380
|
-
setSpinner(new
|
|
309
|
+
setSpinner(new spinner_imperative_1.ImperativeSpinner(first, pool));
|
|
381
310
|
currentSpinner.start();
|
|
382
311
|
},
|
|
383
312
|
onModelStreamStart: () => {
|
|
@@ -424,16 +353,14 @@ function createCliCallbacks() {
|
|
|
424
353
|
}
|
|
425
354
|
const toolPhrases = (0, phrases_1.getPhrases)().tool;
|
|
426
355
|
const first = (0, phrases_1.pickPhrase)(toolPhrases);
|
|
427
|
-
setSpinner(new
|
|
356
|
+
setSpinner(new spinner_imperative_1.ImperativeSpinner(first, toolPhrases));
|
|
428
357
|
currentSpinner.start();
|
|
429
358
|
hadToolRun = true;
|
|
430
359
|
},
|
|
431
360
|
onToolEnd: (name, argSummary, success) => {
|
|
432
361
|
currentSpinner?.stop();
|
|
433
362
|
setSpinner(null);
|
|
434
|
-
|
|
435
|
-
const color = success ? "\x1b[32m" : "\x1b[31m";
|
|
436
|
-
process.stderr.write(`${color}${msg}\x1b[0m\n`);
|
|
363
|
+
(0, tool_display_1.writeToolEnd)(name, argSummary, success);
|
|
437
364
|
},
|
|
438
365
|
onError: (error, severity) => {
|
|
439
366
|
if (severity === "transient") {
|
|
@@ -509,29 +436,191 @@ async function* createDebouncedLines(source, debounceMs) {
|
|
|
509
436
|
yield lines.join("\n");
|
|
510
437
|
}
|
|
511
438
|
}
|
|
439
|
+
/**
|
|
440
|
+
* Async queue that bridges push-based Ink input to pull-based async iteration.
|
|
441
|
+
* Input from Ink's onSubmit callback is pushed; the business logic loop awaits via for-await.
|
|
442
|
+
*/
|
|
443
|
+
class InputQueue {
|
|
444
|
+
queue = [];
|
|
445
|
+
resolve = null;
|
|
446
|
+
done = false;
|
|
447
|
+
push(input) {
|
|
448
|
+
if (this.done)
|
|
449
|
+
return;
|
|
450
|
+
if (this.resolve) {
|
|
451
|
+
const r = this.resolve;
|
|
452
|
+
this.resolve = null;
|
|
453
|
+
r({ value: input, done: false });
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
this.queue.push(input);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
close() {
|
|
460
|
+
this.done = true;
|
|
461
|
+
if (this.resolve) {
|
|
462
|
+
const r = this.resolve;
|
|
463
|
+
this.resolve = null;
|
|
464
|
+
r({ value: undefined, done: true });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
[Symbol.asyncIterator]() {
|
|
468
|
+
return {
|
|
469
|
+
next: () => {
|
|
470
|
+
if (this.queue.length > 0) {
|
|
471
|
+
return Promise.resolve({ value: this.queue.shift(), done: false });
|
|
472
|
+
}
|
|
473
|
+
if (this.done) {
|
|
474
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
475
|
+
}
|
|
476
|
+
return new Promise((resolve) => {
|
|
477
|
+
this.resolve = resolve;
|
|
478
|
+
});
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
exports.InputQueue = InputQueue;
|
|
512
484
|
async function runCliSession(options) {
|
|
513
485
|
/* v8 ignore start -- integration: runCliSession is interactive, tested via E2E @preserve */
|
|
514
|
-
const pasteDebounceMs = options.pasteDebounceMs ?? 50;
|
|
515
486
|
const registry = (0, commands_1.createCommandRegistry)();
|
|
516
487
|
if (!options.disableCommands) {
|
|
517
488
|
(0, commands_1.registerDefaultCommands)(registry);
|
|
518
489
|
}
|
|
519
490
|
const messages = options.messages
|
|
520
491
|
?? [{ role: "system", content: await (0, prompt_1.buildSystem)("cli") }];
|
|
521
|
-
|
|
522
|
-
const
|
|
492
|
+
// ─── Rendering: TUI (Ink + Static) for TTY, imperative for tests/pipes ───
|
|
493
|
+
const useTui = !options._testInputSource && process.stdin.isTTY === true;
|
|
523
494
|
let currentAbort = null;
|
|
524
|
-
const history = [];
|
|
525
495
|
let closed = false;
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
496
|
+
let cliCallbacks;
|
|
497
|
+
let tuiStore = null;
|
|
498
|
+
let inkRef = null;
|
|
499
|
+
const inputQueue = useTui ? new InputQueue() : null;
|
|
500
|
+
let rl = null;
|
|
501
|
+
if (useTui) {
|
|
502
|
+
try {
|
|
503
|
+
const [ink, React, tuiMod, storeMod] = await Promise.all([
|
|
504
|
+
Promise.resolve().then(() => __importStar(require("ink"))),
|
|
505
|
+
Promise.resolve().then(() => __importStar(require("react"))),
|
|
506
|
+
Promise.resolve().then(() => __importStar(require("./cli/ouro-tui"))),
|
|
507
|
+
Promise.resolve().then(() => __importStar(require("./cli/tui-store"))),
|
|
508
|
+
]);
|
|
509
|
+
const { OuroTui } = tuiMod;
|
|
510
|
+
const { TuiStore, createTuiCallbacks } = storeMod;
|
|
511
|
+
tuiStore = new TuiStore();
|
|
512
|
+
cliCallbacks = createTuiCallbacks(tuiStore);
|
|
513
|
+
// Seed input history from previous session (for up/down arrows) — NOT display
|
|
514
|
+
const prevUserMsgs = messages
|
|
515
|
+
.filter((msg) => msg.role === "user" && typeof msg.content === "string")
|
|
516
|
+
.map(msg => msg.content);
|
|
517
|
+
tuiStore.seedHistory(prevUserMsgs);
|
|
518
|
+
// Ctrl-C state machine (Claude Code behavior):
|
|
519
|
+
// During generation: abort current request
|
|
520
|
+
// Idle with text: clear input
|
|
521
|
+
// Idle empty (first): warn
|
|
522
|
+
// Idle empty (second): exit
|
|
523
|
+
let ctrlCWarned = false;
|
|
524
|
+
const handleCtrlC = (hasInput) => {
|
|
525
|
+
if (currentAbort) {
|
|
526
|
+
// During generation: abort the request
|
|
527
|
+
currentAbort.abort();
|
|
528
|
+
ctrlCWarned = false;
|
|
529
|
+
return "abort";
|
|
530
|
+
}
|
|
531
|
+
if (hasInput) {
|
|
532
|
+
ctrlCWarned = false;
|
|
533
|
+
return "clear";
|
|
534
|
+
}
|
|
535
|
+
if (ctrlCWarned) {
|
|
536
|
+
ctrlCWarned = false;
|
|
537
|
+
closed = true;
|
|
538
|
+
inputQueue.close();
|
|
539
|
+
return "exit";
|
|
540
|
+
}
|
|
541
|
+
ctrlCWarned = true;
|
|
542
|
+
return "warn";
|
|
543
|
+
};
|
|
544
|
+
// TUI root: subscribes to store, passes props to OuroTui
|
|
545
|
+
// Elapsed timer is local React state (no store.notify overhead)
|
|
546
|
+
const storeRef = tuiStore;
|
|
547
|
+
function TuiRoot() {
|
|
548
|
+
const [, forceUpdate] = React.useState(0);
|
|
549
|
+
const [elapsed, setElapsed] = React.useState(0);
|
|
550
|
+
React.useEffect(() => storeRef.subscribe(() => {
|
|
551
|
+
forceUpdate((n) => n + 1);
|
|
552
|
+
// Reset ctrlC warning on any state change (new turn, etc.)
|
|
553
|
+
}), []);
|
|
554
|
+
React.useEffect(() => {
|
|
555
|
+
const iv = setInterval(() => setElapsed(storeRef.getElapsed()), 1000);
|
|
556
|
+
return () => clearInterval(iv);
|
|
557
|
+
}, []);
|
|
558
|
+
return React.createElement(OuroTui, {
|
|
559
|
+
agentName: options.agentName,
|
|
560
|
+
model: (0, identity_1.loadAgentConfig)().humanFacing?.model ?? "",
|
|
561
|
+
completedMessages: storeRef.completedMessages,
|
|
562
|
+
inputHistory: storeRef.inputHistory,
|
|
563
|
+
live: storeRef.live,
|
|
564
|
+
elapsedSeconds: elapsed,
|
|
565
|
+
contextPercent: 0,
|
|
566
|
+
onSubmit: (text) => { ctrlCWarned = false; inputQueue.push(text); },
|
|
567
|
+
onCtrlC: handleCtrlC,
|
|
568
|
+
headerShown: storeRef.headerShown,
|
|
569
|
+
cwd: process.cwd().replace(process.env.HOME ?? "", "~"),
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
inkRef = ink.render(React.createElement(TuiRoot), { exitOnCtrlC: false, patchConsole: false });
|
|
573
|
+
}
|
|
574
|
+
catch (err) {
|
|
575
|
+
// Ink failed to load (CJS compat, missing deps, etc.) — fall through to imperative
|
|
576
|
+
(0, runtime_1.emitNervesEvent)({
|
|
577
|
+
component: "senses",
|
|
578
|
+
event: "senses.tui_fallback",
|
|
579
|
+
message: `TUI failed to load, falling back to imperative: ${err instanceof Error ? err.message : String(err)}`,
|
|
580
|
+
meta: {},
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
// Fallback to imperative callbacks if TUI didn't initialize
|
|
585
|
+
if (!tuiStore) {
|
|
586
|
+
cliCallbacks = createCliCallbacks();
|
|
587
|
+
if (options.banner !== false) {
|
|
588
|
+
const bannerText = typeof options.banner === "string"
|
|
589
|
+
? options.banner
|
|
590
|
+
: `${options.agentName} (type /commands for help)`;
|
|
591
|
+
// eslint-disable-next-line no-console -- terminal UX: startup banner
|
|
592
|
+
console.log(`\n${bannerText}\n`);
|
|
593
|
+
}
|
|
533
594
|
}
|
|
534
|
-
|
|
595
|
+
// Display helpers: route to TUI store or imperative stderr/stdout
|
|
596
|
+
const display = {
|
|
597
|
+
error: (msg) => {
|
|
598
|
+
if (tuiStore)
|
|
599
|
+
tuiStore.setError(msg);
|
|
600
|
+
else
|
|
601
|
+
process.stderr.write(`\x1b[31m${msg}\x1b[0m\n`);
|
|
602
|
+
},
|
|
603
|
+
warn: (msg) => {
|
|
604
|
+
if (tuiStore)
|
|
605
|
+
tuiStore.setError(msg); // TUI shows warnings as errors (amber color handled in component)
|
|
606
|
+
else
|
|
607
|
+
process.stderr.write(`\x1b[33m${msg}\x1b[0m\n`);
|
|
608
|
+
},
|
|
609
|
+
text: (msg) => {
|
|
610
|
+
if (tuiStore)
|
|
611
|
+
tuiStore.appendText(msg);
|
|
612
|
+
else
|
|
613
|
+
process.stdout.write(`${msg}\n`);
|
|
614
|
+
},
|
|
615
|
+
suppressInput: () => {
|
|
616
|
+
if (tuiStore)
|
|
617
|
+
tuiStore.suppressInput();
|
|
618
|
+
},
|
|
619
|
+
restoreInput: () => {
|
|
620
|
+
if (tuiStore)
|
|
621
|
+
tuiStore.restoreInput();
|
|
622
|
+
},
|
|
623
|
+
};
|
|
535
624
|
const effectiveToolContext = {
|
|
536
625
|
signin: options.toolContext?.signin ?? (async () => undefined),
|
|
537
626
|
...options.toolContext,
|
|
@@ -543,7 +632,7 @@ async function runCliSession(options) {
|
|
|
543
632
|
};
|
|
544
633
|
messages.push(assistantMessage);
|
|
545
634
|
await options.onAsyncAssistantMessage?.(messages, assistantMessage);
|
|
546
|
-
|
|
635
|
+
display.text(message);
|
|
547
636
|
await options.toolContext?.codingFeedback?.send(message);
|
|
548
637
|
},
|
|
549
638
|
},
|
|
@@ -558,8 +647,6 @@ async function runCliSession(options) {
|
|
|
558
647
|
if (name === options.exitOnToolCall) {
|
|
559
648
|
exitToolResult = result;
|
|
560
649
|
exitToolFired = true;
|
|
561
|
-
// Abort immediately so the model doesn't generate more output
|
|
562
|
-
// (e.g. reasoning about calling settle after complete_adoption)
|
|
563
650
|
currentAbort?.abort();
|
|
564
651
|
}
|
|
565
652
|
return result;
|
|
@@ -567,28 +654,6 @@ async function runCliSession(options) {
|
|
|
567
654
|
: resolvedExecTool;
|
|
568
655
|
// Resolve toolChoiceRequired: use explicit option if set, else fall back to toggle
|
|
569
656
|
const getEffectiveToolChoiceRequired = () => options.toolChoiceRequired !== undefined ? options.toolChoiceRequired : (0, commands_1.getToolChoiceRequired)();
|
|
570
|
-
// Ctrl-C at the input prompt: clear line or warn/exit
|
|
571
|
-
rl.on("SIGINT", () => {
|
|
572
|
-
const rlInt = rl;
|
|
573
|
-
const currentLine = rlInt.line || "";
|
|
574
|
-
const result = handleSigint(rl, currentLine);
|
|
575
|
-
if (result === "clear") {
|
|
576
|
-
rlInt.line = "";
|
|
577
|
-
rlInt.cursor = 0;
|
|
578
|
-
process.stdout.write(`\r\x1b[K${CLI_PROMPT}`);
|
|
579
|
-
}
|
|
580
|
-
else if (result === "warn") {
|
|
581
|
-
rlInt.line = "";
|
|
582
|
-
rlInt.cursor = 0;
|
|
583
|
-
process.stdout.write("\r\x1b[K");
|
|
584
|
-
process.stderr.write("press Ctrl-C again to exit\n");
|
|
585
|
-
process.stdout.write(CLI_PROMPT);
|
|
586
|
-
}
|
|
587
|
-
else {
|
|
588
|
-
rl.close();
|
|
589
|
-
}
|
|
590
|
-
});
|
|
591
|
-
const debouncedLines = (source) => createDebouncedLines(source, pasteDebounceMs);
|
|
592
657
|
(0, runtime_1.emitNervesEvent)({
|
|
593
658
|
component: "senses",
|
|
594
659
|
event: "senses.cli_session_start",
|
|
@@ -601,7 +666,7 @@ async function runCliSession(options) {
|
|
|
601
666
|
if (options.autoFirstTurn && messages.length > 0 && messages[messages.length - 1]?.role === "user") {
|
|
602
667
|
currentAbort = new AbortController();
|
|
603
668
|
const traceId = (0, nerves_1.createTraceId)();
|
|
604
|
-
|
|
669
|
+
display.suppressInput();
|
|
605
670
|
let result;
|
|
606
671
|
try {
|
|
607
672
|
result = await (0, core_1.runAgent)(messages, cliCallbacks, options.skipSystemPromptRefresh ? undefined : "cli", currentAbort.signal, {
|
|
@@ -613,51 +678,60 @@ async function runCliSession(options) {
|
|
|
613
678
|
});
|
|
614
679
|
}
|
|
615
680
|
catch (err) {
|
|
616
|
-
// AbortError (Ctrl-C) -- silently continue to prompt
|
|
617
|
-
// All other errors: show the user what happened
|
|
618
681
|
if (!(err instanceof DOMException && err.name === "AbortError")) {
|
|
619
|
-
|
|
682
|
+
display.error(err instanceof Error ? err.message : String(err));
|
|
620
683
|
}
|
|
621
684
|
}
|
|
622
685
|
cliCallbacks.flushMarkdown();
|
|
623
|
-
|
|
686
|
+
display.restoreInput();
|
|
624
687
|
currentAbort = null;
|
|
625
688
|
if (exitToolFired) {
|
|
626
689
|
exitReason = "tool_exit";
|
|
627
|
-
|
|
690
|
+
closed = true;
|
|
628
691
|
}
|
|
629
692
|
else {
|
|
630
693
|
const lastMsg = messages[messages.length - 1];
|
|
631
694
|
if (lastMsg?.role === "assistant" && !(typeof lastMsg.content === "string" ? lastMsg.content : "").trim()) {
|
|
632
|
-
|
|
695
|
+
display.warn("(empty response)");
|
|
633
696
|
}
|
|
634
|
-
process.stdout.write("\n\n");
|
|
635
697
|
if (options.onTurnEnd) {
|
|
636
698
|
await options.onTurnEnd(messages, result ?? { usage: undefined });
|
|
637
699
|
}
|
|
638
700
|
}
|
|
639
701
|
}
|
|
640
|
-
if (!exitToolFired) {
|
|
641
|
-
process.stdout.write(CLI_PROMPT);
|
|
642
|
-
}
|
|
643
702
|
try {
|
|
644
|
-
for
|
|
703
|
+
// Input source: TUI queue for Ink, test source for tests, readline fallback
|
|
704
|
+
let inputSource;
|
|
705
|
+
let inputCtrl = null;
|
|
706
|
+
if (tuiStore && inputQueue) {
|
|
707
|
+
// TUI path: input comes from Ink's onSubmit → InputQueue
|
|
708
|
+
inputSource = inputQueue;
|
|
709
|
+
}
|
|
710
|
+
else if (options._testInputSource) {
|
|
711
|
+
inputSource = options._testInputSource;
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
// Imperative fallback: readline
|
|
715
|
+
const isTTY = process.stdin.isTTY === true;
|
|
716
|
+
rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: isTTY });
|
|
717
|
+
inputCtrl = new InputController(rl);
|
|
718
|
+
inputSource = rl;
|
|
719
|
+
process.stdout.write(CLI_PROMPT);
|
|
720
|
+
}
|
|
721
|
+
for await (const input of inputSource) {
|
|
645
722
|
if (closed)
|
|
646
723
|
break;
|
|
647
|
-
if (!input.trim())
|
|
648
|
-
process.stdout.write(CLI_PROMPT);
|
|
724
|
+
if (!input.trim())
|
|
649
725
|
continue;
|
|
650
|
-
}
|
|
651
726
|
// Optional input gate (e.g. trust gate in main)
|
|
652
727
|
if (options.onInput) {
|
|
653
728
|
const gate = options.onInput(input);
|
|
654
729
|
if (!gate.allowed) {
|
|
655
730
|
if (gate.reply) {
|
|
656
|
-
|
|
731
|
+
display.text(gate.reply);
|
|
657
732
|
}
|
|
658
733
|
if (closed)
|
|
659
734
|
break;
|
|
660
|
-
process.stdout.write(CLI_PROMPT);
|
|
661
735
|
continue;
|
|
662
736
|
}
|
|
663
737
|
}
|
|
@@ -676,30 +750,27 @@ async function runCliSession(options) {
|
|
|
676
750
|
await options.onNewSession?.();
|
|
677
751
|
// eslint-disable-next-line no-console -- terminal UX: session cleared
|
|
678
752
|
console.log("session cleared");
|
|
679
|
-
process.stdout.write(CLI_PROMPT);
|
|
680
753
|
continue;
|
|
681
754
|
}
|
|
682
755
|
else if (dispatchResult.result.action === "response") {
|
|
683
|
-
|
|
684
|
-
console.log(dispatchResult.result.message || "");
|
|
685
|
-
process.stdout.write(CLI_PROMPT);
|
|
756
|
+
display.text(dispatchResult.result.message || "");
|
|
686
757
|
continue;
|
|
687
758
|
}
|
|
688
759
|
}
|
|
689
760
|
}
|
|
690
761
|
}
|
|
691
|
-
//
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
addHistory(history, input);
|
|
762
|
+
// Track user message in TUI for display
|
|
763
|
+
if (tuiStore)
|
|
764
|
+
tuiStore.addUserMessage(input);
|
|
695
765
|
currentAbort = new AbortController();
|
|
696
|
-
|
|
766
|
+
if (tuiStore)
|
|
767
|
+
tuiStore.suppressInput();
|
|
768
|
+
inputCtrl?.suppress(() => { currentAbort?.abort(); });
|
|
697
769
|
let result;
|
|
698
770
|
try {
|
|
699
771
|
if (options.runTurn) {
|
|
700
772
|
// Pipeline-based turn: the runTurn callback handles user message assembly,
|
|
701
773
|
// pending drain, trust gate, runAgent, postTurn, and token accumulation.
|
|
702
|
-
// Commands (e.g. /debug, /exit, /new) are intercepted in the pipeline.
|
|
703
774
|
result = await options.runTurn(messages, input, cliCallbacks, currentAbort.signal, effectiveToolContext);
|
|
704
775
|
// Handle pipeline-intercepted commands with loop-control side effects
|
|
705
776
|
if (result?.turnOutcome === "command") {
|
|
@@ -712,12 +783,10 @@ async function runCliSession(options) {
|
|
|
712
783
|
await options.onNewSession?.();
|
|
713
784
|
// eslint-disable-next-line no-console -- terminal UX: session cleared
|
|
714
785
|
console.log("session cleared");
|
|
715
|
-
process.stdout.write(CLI_PROMPT);
|
|
716
786
|
continue;
|
|
717
787
|
}
|
|
718
788
|
// For "response" commands: the pipeline already emitted the response via onTextChunk
|
|
719
789
|
cliCallbacks.flushMarkdown();
|
|
720
|
-
process.stdout.write(CLI_PROMPT);
|
|
721
790
|
continue;
|
|
722
791
|
}
|
|
723
792
|
}
|
|
@@ -736,14 +805,16 @@ async function runCliSession(options) {
|
|
|
736
805
|
}
|
|
737
806
|
}
|
|
738
807
|
catch (err) {
|
|
739
|
-
// AbortError (Ctrl-C) -- silently return to prompt
|
|
740
|
-
// All other errors: show the user what happened
|
|
741
808
|
if (!(err instanceof DOMException && err.name === "AbortError")) {
|
|
742
|
-
|
|
809
|
+
display.error(err instanceof Error ? err.message : String(err));
|
|
743
810
|
}
|
|
744
811
|
}
|
|
745
812
|
cliCallbacks.flushMarkdown();
|
|
746
|
-
|
|
813
|
+
if (!tuiStore)
|
|
814
|
+
process.stdout.write("\n"); // ensure response ends with newline before prompt (imperative only)
|
|
815
|
+
if (tuiStore)
|
|
816
|
+
tuiStore.restoreInput();
|
|
817
|
+
inputCtrl?.restore();
|
|
747
818
|
currentAbort = null;
|
|
748
819
|
// Check if exit tool was fired during this turn
|
|
749
820
|
if (exitToolFired) {
|
|
@@ -753,20 +824,21 @@ async function runCliSession(options) {
|
|
|
753
824
|
// Safety net: never silently swallow an empty response
|
|
754
825
|
const lastMsg = messages[messages.length - 1];
|
|
755
826
|
if (lastMsg?.role === "assistant" && !(typeof lastMsg.content === "string" ? lastMsg.content : "").trim()) {
|
|
756
|
-
|
|
827
|
+
display.warn("(empty response)");
|
|
757
828
|
}
|
|
758
|
-
process.stdout.write("\n\n");
|
|
759
829
|
// Post-turn hook (session persistence, pending drain, prompt refresh, etc.)
|
|
760
830
|
if (options.onTurnEnd) {
|
|
761
831
|
await options.onTurnEnd(messages, result ?? { usage: undefined });
|
|
762
832
|
}
|
|
763
833
|
if (closed)
|
|
764
834
|
break;
|
|
765
|
-
process.stdout.write(CLI_PROMPT);
|
|
766
835
|
}
|
|
767
836
|
}
|
|
768
837
|
finally {
|
|
769
|
-
rl
|
|
838
|
+
rl?.close();
|
|
839
|
+
if (inkRef) {
|
|
840
|
+
inkRef.unmount();
|
|
841
|
+
}
|
|
770
842
|
if (options.banner !== false) {
|
|
771
843
|
// eslint-disable-next-line no-console -- terminal UX: goodbye
|
|
772
844
|
console.log("bye");
|
|
@@ -838,6 +910,7 @@ async function main(agentName, options) {
|
|
|
838
910
|
agentName: currentAgentName,
|
|
839
911
|
pasteDebounceMs,
|
|
840
912
|
messages: sessionMessages,
|
|
913
|
+
_testInputSource: options?._testInputSource,
|
|
841
914
|
onAsyncAssistantMessage: async (messages, _assistantMessage) => {
|
|
842
915
|
(0, context_1.postTurn)(messages, sessPath, undefined, undefined, sessionState);
|
|
843
916
|
},
|
|
@@ -933,3 +1006,4 @@ async function main(agentName, options) {
|
|
|
933
1006
|
sessionLock?.release();
|
|
934
1007
|
}
|
|
935
1008
|
}
|
|
1009
|
+
// CI trigger
|