nexrall-code 0.5.107 → 0.5.108

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,116 @@
1
+ import { EventEmitter } from 'events';
2
+ // ─── readline.Interface-shaped Adapter over Ink ────────────────────────────
3
+ //
4
+ // chat.ts's ~1500 lines of business logic (slash commands, session
5
+ // persistence, agent-turn orchestration…) only ever touch 7 members of the
6
+ // readline.Interface surface: `.on('line', …)`, `.line`, `.cursor`, `.pause()`,
7
+ // `.resume()`, `.prompt()`, `.close()` — plus `.question(query, cb)` for the
8
+ // shared-interface permission-prompt reuse in permissions/handler.ts. Rather
9
+ // than rewriting every one of those call sites to talk to Ink directly (a
10
+ // much larger, much riskier change), this adapter implements exactly that
11
+ // surface backed by the Ink app's input state — so chat.ts's existing
12
+ // `rl.on('line', …)`/`rl.pause()`/`rl.resume()` code keeps working verbatim,
13
+ // it just now reads from/writes to Ink's React state under the hood instead
14
+ // of a real readline.Interface.
15
+ //
16
+ // ─── `pause()`/`resume()` no longer disable the input box ──────────────────
17
+ //
18
+ // They used to call `ink.setInputEnabled(false/true)`, which made Ink's
19
+ // `useInput` bail out on EVERY keystroke while an agent turn was running —
20
+ // no typing, no cursor movement, and (the worst of it) no Ctrl+C and no
21
+ // answering a permission y/n prompt fired mid-turn via `question()`, because
22
+ // that prompt is answered through the SAME input pipeline. A permission
23
+ // question asked while `rl.pause()` was in effect could never be answered at
24
+ // all — verified directly with an isolated harness that called `askLine()`
25
+ // after `setInputEnabled(false)`: it hung indefinitely.
26
+ //
27
+ // `pause()`/`resume()` now toggle `ink.setBusy()` instead, which keeps every
28
+ // keystroke flowing (see inkTerminal.tsx's useInput) and only changes what a
29
+ // submitted line does: while busy, Enter without an outstanding askLine()
30
+ // question queues the text (mirrors the VS Code panel's `_pendingInjections`)
31
+ // instead of emitting `'line'` — which would otherwise start a SECOND
32
+ // concurrent turn on top of the one already running.
33
+ export class InkReadlineAdapter extends EventEmitter {
34
+ // Kept for API parity with readline.Interface; Ink owns the actual typed
35
+ // text internally (the input box's own React state), so these are not
36
+ // read from live — chat.ts never reads them for this adapter's usage.
37
+ line = '';
38
+ cursor = 0;
39
+ ink;
40
+ busy = false;
41
+ pendingInput = [];
42
+ constructor(ink) {
43
+ super();
44
+ this.ink = ink;
45
+ this.ink.onLine((text) => {
46
+ // Historically this checked `this.paused` and dropped the line
47
+ // entirely — the mechanism that made typing during a turn a dead end.
48
+ // Ordinary (non-busy) submits always reach chat.ts's `rl.on('line', …)`
49
+ // now; busy-time submits go through onQueuedLine below instead, so
50
+ // this handler firing at all already implies "not busy" (inkTerminal's
51
+ // Enter branch routes to onLine vs onQueuedLine based on the same
52
+ // `busy` flag chat.ts sets here).
53
+ this.emit('line', text);
54
+ });
55
+ this.ink.onQueuedLine((text) => {
56
+ this.pendingInput.push(text);
57
+ });
58
+ this.ink.onInterrupt(() => {
59
+ this.emit('interrupt');
60
+ });
61
+ // Ink is exiting (a confirmed Ctrl+C quit, or stdin ending). A real
62
+ // readline.Interface emits 'close' when its input ends, and chat.ts's
63
+ // `rl.on('close')` is the ONLY place an interactive session persists the
64
+ // conversation on the way out — it saves, prints "Goodbye." and exits.
65
+ //
66
+ // Nothing used to trigger it for this adapter: `close()` below called
67
+ // straight through to Ink and never emitted the event, and the main Ink
68
+ // app's exit was never observed at all (no waitUntilExit subscriber). So
69
+ // quitting an interactive session with Ctrl+C silently DISCARDED the
70
+ // whole conversation — verified with an isolated harness: rl.close()
71
+ // fired no 'close' listener. That was survivable while Ctrl+C was an
72
+ // undocumented instant-kill; now that it is the confirmed, deliberate way
73
+ // to leave a session, losing the transcript is not acceptable.
74
+ this.ink.onExit(() => {
75
+ this.emit('close');
76
+ });
77
+ }
78
+ prompt(_preserveCursor) {
79
+ // No-op: Ink re-renders the input box reactively on every state change,
80
+ // there is no separate "draw the prompt now" step to trigger — unlike
81
+ // the previous hand-rolled split-screen renderer, which needed an
82
+ // explicit redraw call at specific points.
83
+ }
84
+ pause() {
85
+ this.busy = true;
86
+ this.ink.setBusy(true);
87
+ return this;
88
+ }
89
+ resume() {
90
+ this.busy = false;
91
+ this.ink.setBusy(false);
92
+ return this;
93
+ }
94
+ close() {
95
+ this.ink.close();
96
+ }
97
+ /** Compatible with readline.Interface#question — used by permissions/handler.ts's shared-interface reuse. */
98
+ question(query, cb) {
99
+ this.ink.askLine(query).then(cb);
100
+ }
101
+ /**
102
+ * Drains and returns every follow-up message the user typed and sent while
103
+ * `pause()` was active (i.e. during a running agent turn). Intended to be
104
+ * passed straight through as `AgentLoopOptions.takePendingInput` — see
105
+ * loop.ts, and the VS Code panel's `_pendingInjections.splice(0)` for the
106
+ * pattern this mirrors. Returns `[]` when nothing is queued, and empties
107
+ * the queue on every call so the same follow-up is never folded in twice.
108
+ */
109
+ takePendingInput() {
110
+ return this.pendingInput.splice(0);
111
+ }
112
+ /** True while a `pause()`/`resume()` pair is in effect (an agent turn is running). */
113
+ isBusy() {
114
+ return this.busy;
115
+ }
116
+ }