aiden-runtime 4.7.0 → 4.8.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.
- package/README.md +12 -1
- package/dist/cli/v4/aidenCLI.js +40 -5
- package/dist/cli/v4/callbacks.js +52 -31
- package/dist/cli/v4/chatSession.js +55 -8
- package/dist/cli/v4/commands/help.js +22 -11
- package/dist/cli/v4/commands/runs.js +42 -24
- package/dist/cli/v4/commands/skills.js +15 -17
- package/dist/cli/v4/commands/update.js +14 -2
- package/dist/cli/v4/commands/usage.js +17 -5
- package/dist/cli/v4/daemonAgentBuilder.js +1 -0
- package/dist/cli/v4/design/tokens.js +265 -0
- package/dist/cli/v4/display/framedPanel.js +116 -0
- package/dist/cli/v4/display/toolTrail.js +2 -2
- package/dist/cli/v4/display.js +489 -164
- package/dist/cli/v4/onboarding/disclaimer.js +42 -10
- package/dist/cli/v4/onboarding/loading.js +24 -1
- package/dist/cli/v4/onboarding/successScreen.js +17 -8
- package/dist/cli/v4/pasteIntercept.js +214 -70
- package/dist/cli/v4/replyRenderer.js +213 -58
- package/dist/cli/v4/setupWizard.js +19 -2
- package/dist/cli/v4/skinEngine.js +13 -0
- package/dist/cli/v4/table.js +65 -8
- package/dist/core/v4/aidenAgent.js +23 -0
- package/dist/core/v4/auxiliaryClient.js +46 -13
- package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +13 -8
- package/dist/core/v4/promptBuilder.js +51 -0
- package/dist/core/v4/subagent/childBuilder.js +1 -0
- package/dist/core/v4/subagent/spawnSubAgent.js +7 -1
- package/dist/core/v4/ui/banner.js +16 -16
- package/dist/core/v4/update/executeInstall.js +10 -6
- package/dist/core/v4/update/installMethodDetect.js +7 -0
- package/dist/core/version.js +67 -2
- package/dist/moat/approvalEngine.js +14 -0
- package/dist/tools/v4/index.js +54 -0
- package/dist/tools/v4/subagent/spawnSubAgentTool.js +23 -0
- package/package.json +1 -3
|
@@ -60,9 +60,24 @@ const readline = __importStar(require("node:readline"));
|
|
|
60
60
|
const banner_1 = require("../../../core/v4/ui/banner");
|
|
61
61
|
const theme_1 = require("../../../core/v4/ui/theme");
|
|
62
62
|
const version_1 = require("../../../core/version");
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
// v4.8.0 Slice 10c — replaced the single-paragraph prose with two
|
|
64
|
+
// scannable bullet lists (capability + acknowledgments). Legal terms
|
|
65
|
+
// surfaced as a checklist instead of buried inside prose.
|
|
66
|
+
const DISCLAIMER_HEAD = 'Aiden is an autonomous AI engine that runs on your machine. Aiden can:';
|
|
67
|
+
const CAPABILITY_BULLETS = [
|
|
68
|
+
'Read, write, and modify files on your computer',
|
|
69
|
+
'Execute shell commands and run code',
|
|
70
|
+
'Browse the web and interact with online services',
|
|
71
|
+
'Connect to AI providers using YOUR API keys (BYOK)',
|
|
72
|
+
'Generate and execute new skills based on your prompts',
|
|
73
|
+
];
|
|
74
|
+
const ACK_HEAD = 'By continuing, you acknowledge:';
|
|
75
|
+
const ACK_BULLETS = [
|
|
76
|
+
'Aiden operates on your behalf with full local-system access',
|
|
77
|
+
'You are responsible for outcomes of commands you approve',
|
|
78
|
+
'Open source under AGPL-3.0 — read the code at github.com/taracodlabs/aiden',
|
|
79
|
+
'This is beta software, built solo, still rough in spots',
|
|
80
|
+
];
|
|
66
81
|
/**
|
|
67
82
|
* Word-wrap `text` to `width` columns. Preserves single spaces; does
|
|
68
83
|
* not handle ANSI codes (callers pass plain text here).
|
|
@@ -97,18 +112,35 @@ function clearScreen(out) {
|
|
|
97
112
|
out.write('\x1b[2J\x1b[H');
|
|
98
113
|
}
|
|
99
114
|
/**
|
|
100
|
-
* Render the disclaimer body —
|
|
101
|
-
*
|
|
115
|
+
* Render the disclaimer body — v4.8.0 Slice 10c: banner + framed-panel
|
|
116
|
+
* capability list + acknowledgments. Orange `▎` bar on every line of
|
|
117
|
+
* the panel matches the rest of v4.8.0 chrome. `▸` bullets keep
|
|
118
|
+
* capability/ack items scannable rather than buried in prose.
|
|
102
119
|
*/
|
|
103
120
|
function renderDisclaimerBody(version) {
|
|
104
121
|
const w = (0, theme_1.termWidth)();
|
|
122
|
+
const innerW = Math.min(w - 4, 70);
|
|
105
123
|
const body = [];
|
|
106
124
|
body.push((0, banner_1.renderBanner)({ version }));
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
125
|
+
// Slice 10c framed-panel chrome. Orange bar at col 2; content + 2
|
|
126
|
+
// inner spaces; muted `─` divider between sections.
|
|
127
|
+
// v4.8.0 Slice 11c — `▎` swapped for `│` (U+2502); same swap as
|
|
128
|
+
// glyphs.panel.bar in the design tokens.
|
|
129
|
+
const bar = theme_1.c.primary('│');
|
|
130
|
+
const divider = theme_1.c.muted('─'.repeat(innerW - 2));
|
|
131
|
+
const line = (s) => ` ${bar} ${s}\n`;
|
|
132
|
+
body.push(line(theme_1.c.text(DISCLAIMER_HEAD)));
|
|
133
|
+
body.push(line(''));
|
|
134
|
+
for (const item of CAPABILITY_BULLETS) {
|
|
135
|
+
body.push(line(theme_1.c.muted('▸ ') + theme_1.c.text(item)));
|
|
136
|
+
}
|
|
137
|
+
body.push(line(''));
|
|
138
|
+
body.push(line(divider));
|
|
139
|
+
body.push(line(''));
|
|
140
|
+
body.push(line(theme_1.c.text(ACK_HEAD)));
|
|
141
|
+
body.push(line(''));
|
|
142
|
+
for (const item of ACK_BULLETS) {
|
|
143
|
+
body.push(line(theme_1.c.muted('▸ ') + theme_1.c.text(item)));
|
|
112
144
|
}
|
|
113
145
|
body.push('\n');
|
|
114
146
|
return body.join('');
|
|
@@ -86,13 +86,29 @@ async function runLoadingSequence(steps, opts = {}) {
|
|
|
86
86
|
return { ok: results.every((r) => r.ok), steps: results };
|
|
87
87
|
}
|
|
88
88
|
out.write('\n ' + theme_1.c.text(heading) + '\n\n');
|
|
89
|
+
// v4.8.0 Slice 10c — progress bar above the step rows. 10 cells
|
|
90
|
+
// (●/○) split proportionally across the steps; each completed step
|
|
91
|
+
// fills floor(10 * (i+1) / N) cells. Uses the same hex-dot glyphs
|
|
92
|
+
// as the status footer's context bar for visual consistency.
|
|
93
|
+
const BAR_CELLS = 10;
|
|
94
|
+
const buildBar = (completed) => {
|
|
95
|
+
const filled = Math.min(BAR_CELLS, Math.floor((BAR_CELLS * completed) / steps.length));
|
|
96
|
+
const pct = Math.round((completed / steps.length) * 100);
|
|
97
|
+
const fillSeg = theme_1.c.primary('●'.repeat(filled));
|
|
98
|
+
const emptySeg = theme_1.c.muted('○'.repeat(BAR_CELLS - filled));
|
|
99
|
+
const label = completed < steps.length
|
|
100
|
+
? theme_1.c.muted(steps[completed].label + '...')
|
|
101
|
+
: theme_1.c.muted('done');
|
|
102
|
+
return ` ${fillSeg}${emptySeg} ${theme_1.c.text(String(pct).padStart(3) + '%')} ${label}`;
|
|
103
|
+
};
|
|
104
|
+
out.write(buildBar(0) + '\n\n');
|
|
89
105
|
// Pre-paint placeholder rows so the spinner overwrites in place.
|
|
90
106
|
for (const step of steps) {
|
|
91
107
|
const line = ' ' + theme_1.c.muted('·') + ' ' + rpad(step.label, labelCol) +
|
|
92
108
|
' ' + theme_1.c.muted(lpad('—', statusCol));
|
|
93
109
|
out.write(line + '\n');
|
|
94
110
|
}
|
|
95
|
-
// Walk back up to the top of the block.
|
|
111
|
+
// Walk back up to the top of the step block.
|
|
96
112
|
out.write(`\x1b[${steps.length}A`);
|
|
97
113
|
for (let i = 0; i < steps.length; i++) {
|
|
98
114
|
const step = steps[i];
|
|
@@ -127,6 +143,13 @@ async function runLoadingSequence(steps, opts = {}) {
|
|
|
127
143
|
' ' + lpad(statusText, statusCol) + ' ' + timing;
|
|
128
144
|
out.write('\x1b[2K\r' + row + '\n');
|
|
129
145
|
results.push({ label: step.label, ok, status, ms });
|
|
146
|
+
// v4.8.0 Slice 10c — repaint the progress bar above the step
|
|
147
|
+
// block after each step completes. Cursor is currently on the
|
|
148
|
+
// line below the just-completed step; walk up to the bar line
|
|
149
|
+
// (steps.length - i - 1 rows of remaining steps + 1 blank line
|
|
150
|
+
// separator + the bar itself), rewrite, then walk back down.
|
|
151
|
+
const upCount = (steps.length - i - 1) + 2;
|
|
152
|
+
out.write(`\x1b[${upCount}A\x1b[2K\r${buildBar(i + 1)}\x1b[${upCount}B\r`);
|
|
130
153
|
}
|
|
131
154
|
out.write('\n ' + (0, theme_1.separator)(Math.min(w - 4, 64)) + '\n');
|
|
132
155
|
return { ok: results.every((r) => r.ok), steps: results };
|
|
@@ -47,22 +47,31 @@ function renderSuccessScreen(opts = {}) {
|
|
|
47
47
|
out.write('setup-complete\n');
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
|
+
// v4.8.0 Slice 10b — Aiden-native framed panel chrome. Each row
|
|
51
|
+
// carries the orange bar; content (title + examples + closing
|
|
52
|
+
// hint) preserved verbatim so content-level test assertions hold.
|
|
53
|
+
// v4.8.0 Slice 11c — `▎` swapped for `│` (U+2502); same swap as
|
|
54
|
+
// glyphs.panel.bar in the design tokens.
|
|
50
55
|
const w = (0, theme_1.termWidth)();
|
|
51
56
|
const sepW = Math.min(w - 4, 64);
|
|
52
57
|
const narrow = w < 60;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
const bar = theme_1.c.primary('│');
|
|
59
|
+
const divider = theme_1.c.muted('─'.repeat(sepW - 2));
|
|
60
|
+
const line = (s) => ` ${bar} ${s}`;
|
|
56
61
|
out.write('\n');
|
|
62
|
+
out.write(line((0, theme_1.bold)(theme_1.c.primary('All set!'))) + '\n');
|
|
63
|
+
out.write(line(divider) + '\n');
|
|
64
|
+
out.write(line(theme_1.c.text('Aiden is ready. Try these to start:')) + '\n');
|
|
65
|
+
out.write(line('') + '\n');
|
|
57
66
|
if (narrow) {
|
|
58
|
-
|
|
59
|
-
out.write(' ' + theme_1.c.muted('▸ ') + theme_1.c.accent(examples[0]) + '\n');
|
|
67
|
+
out.write(line(theme_1.c.muted('▸ ') + theme_1.c.accent(examples[0])) + '\n');
|
|
60
68
|
}
|
|
61
69
|
else {
|
|
62
70
|
for (const ex of examples) {
|
|
63
|
-
out.write(
|
|
71
|
+
out.write(line(theme_1.c.muted('▸ ') + theme_1.c.accent(ex)) + '\n');
|
|
64
72
|
}
|
|
65
73
|
}
|
|
66
|
-
out.write(
|
|
67
|
-
out.write('
|
|
74
|
+
out.write(line('') + '\n');
|
|
75
|
+
out.write(line(theme_1.c.muted('Or just say hi.')) + '\n');
|
|
76
|
+
out.write('\n');
|
|
68
77
|
}
|
|
@@ -6,19 +6,52 @@
|
|
|
6
6
|
* Aiden — local-first agent.
|
|
7
7
|
*/
|
|
8
8
|
/**
|
|
9
|
-
* cli/v4/pasteIntercept.ts —
|
|
9
|
+
* cli/v4/pasteIntercept.ts — stdin pre-tap for bracketed paste.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
11
|
+
* Modern @inquirer/prompts treats any embedded `\n` as Enter and
|
|
12
|
+
* resolves early, so a multi-line paste would auto-submit one line
|
|
13
|
+
* at a time. This module intercepts paste payloads BEFORE inquirer
|
|
14
|
+
* sees them, persists them to a manifest, and substitutes a
|
|
15
|
+
* `[paste #<id>: <N> lines, <bytes>]` label on stdin. The user sees
|
|
16
|
+
* the label inside inquirer's input buffer, edits it like any other
|
|
17
|
+
* text, then presses Enter to submit; `chatSession.readUserInput`
|
|
18
|
+
* swaps the label back for the original via `getPasteOriginal(id)`
|
|
19
|
+
* before handing to the agent.
|
|
18
20
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
21
|
+
* v4.8.1 Slice 2 hotfix #6 — robustness rebuild for terminal-
|
|
22
|
+
* environment diversity:
|
|
23
|
+
*
|
|
24
|
+
* • State machine survives reads split across chunk boundaries.
|
|
25
|
+
* The begin or end marker can arrive partially in one chunk
|
|
26
|
+
* and be completed by the next; the parser keeps state in `buf`
|
|
27
|
+
* until a full marker is observed.
|
|
28
|
+
*
|
|
29
|
+
* • 800ms watchdog flushes a stuck `in_marker_paste` state if
|
|
30
|
+
* the terminal never delivers PASTE_END (mosh/tmux/SSH paths
|
|
31
|
+
* have all been observed to drop end markers under load).
|
|
32
|
+
*
|
|
33
|
+
* • Degraded marker forms get normalised to canonical at the
|
|
34
|
+
* intercept boundary. Visible-escape variants (`^[[200~`) are
|
|
35
|
+
* the common case from terminals that escape control sequences
|
|
36
|
+
* for display.
|
|
37
|
+
*
|
|
38
|
+
* • CRLF/CR → LF normalisation is applied universally on every
|
|
39
|
+
* incoming chunk, not just inside marker payloads. Some
|
|
40
|
+
* clipboard payloads carry CR-only line endings.
|
|
41
|
+
*
|
|
42
|
+
* • 30ms timing accumulation catches line-by-line paste delivery
|
|
43
|
+
* — the failure mode that surfaced after hotfix #5. When a
|
|
44
|
+
* terminal delivers a paste as N small `"<line>\n"` chunks
|
|
45
|
+
* instead of one bulk chunk, each chunk has a single trailing
|
|
46
|
+
* `\n` and would otherwise pass through as an Enter keystroke.
|
|
47
|
+
* The accumulator holds candidate chunks (`length > 1` so the
|
|
48
|
+
* bare Enter keystroke `"\n"` is never held) for a 30ms window;
|
|
49
|
+
* if another candidate arrives, both are accumulated as a
|
|
50
|
+
* multi-line paste and substituted with the placeholder before
|
|
51
|
+
* any `\n` reaches inquirer. If no follow-up arrives within the
|
|
52
|
+
* window, the held chunk is emitted unchanged (normal Enter).
|
|
53
|
+
* 30ms is imperceptible to humans and well below sustained
|
|
54
|
+
* keystroke timing.
|
|
22
55
|
*/
|
|
23
56
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
24
57
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
@@ -33,7 +66,16 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
33
66
|
const paths_1 = require("../../core/v4/paths");
|
|
34
67
|
const PASTE_BEGIN = '\x1b[200~';
|
|
35
68
|
const PASTE_END = '\x1b[201~';
|
|
36
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Degraded marker patterns observed in the wild. Each is rewritten
|
|
71
|
+
* to canonical at the normalisation boundary so the parser only
|
|
72
|
+
* needs to know about one form.
|
|
73
|
+
*/
|
|
74
|
+
const DEGRADED_BEGIN = /\^\[\[200~/g;
|
|
75
|
+
const DEGRADED_END = /\^\[\[201~/g;
|
|
76
|
+
const ACCUMULATION_MS = 30;
|
|
77
|
+
const WATCHDOG_MS = 800;
|
|
78
|
+
/** id → original text (in-memory swap table). Disk has /pastes/paste_<id>.txt as source of truth for /show. */
|
|
37
79
|
const originals = new Map();
|
|
38
80
|
function pastesDir() {
|
|
39
81
|
return node_path_1.default.join((0, paths_1.resolveAidenPaths)().root, 'pastes');
|
|
@@ -74,18 +116,17 @@ function compressSync(text) {
|
|
|
74
116
|
}
|
|
75
117
|
/**
|
|
76
118
|
* Look up the original text for a paste id. Returns undefined if the
|
|
77
|
-
* id was never seen by this process
|
|
78
|
-
*
|
|
79
|
-
*
|
|
119
|
+
* id was never seen by this process. Disk (/pastes/paste_<id>.txt)
|
|
120
|
+
* is the source of truth for /show <id>; this map is the fast path
|
|
121
|
+
* for the in-flight prompt swap.
|
|
80
122
|
*/
|
|
81
123
|
function getPasteOriginal(id) {
|
|
82
124
|
return originals.get(id);
|
|
83
125
|
}
|
|
84
126
|
/**
|
|
85
127
|
* Replace `[paste #N: …]` patterns in `input` with the corresponding
|
|
86
|
-
* original text
|
|
87
|
-
*
|
|
88
|
-
* string.
|
|
128
|
+
* original text. Patterns whose id we don't know are left intact
|
|
129
|
+
* (might be user-typed by hand).
|
|
89
130
|
*/
|
|
90
131
|
function expandPasteLabels(input) {
|
|
91
132
|
return input.replace(/\[paste #(\d+):[^\]]*\]/g, (m, id) => {
|
|
@@ -93,6 +134,38 @@ function expandPasteLabels(input) {
|
|
|
93
134
|
return orig !== undefined ? orig : m;
|
|
94
135
|
});
|
|
95
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Universal normalisation applied at the intercept boundary:
|
|
139
|
+
* CRLF + bare CR → LF, then degraded marker variants → canonical.
|
|
140
|
+
*/
|
|
141
|
+
function normalize(text) {
|
|
142
|
+
let t = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
143
|
+
t = t.replace(DEGRADED_BEGIN, PASTE_BEGIN);
|
|
144
|
+
t = t.replace(DEGRADED_END, PASTE_END);
|
|
145
|
+
return t;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Decide whether `payload` should emit inline (small single-line) or
|
|
149
|
+
* be funnelled through the disk-backed placeholder system. Same
|
|
150
|
+
* thresholds for marker-wrapped and timing-accumulated paths so the
|
|
151
|
+
* user sees identical chrome regardless of how the paste arrived.
|
|
152
|
+
*/
|
|
153
|
+
function payloadToEmission(payload) {
|
|
154
|
+
const trimmed = payload.replace(/\n+$/, '');
|
|
155
|
+
if (!trimmed.includes('\n') && trimmed.length <= 500) {
|
|
156
|
+
return trimmed;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const { id, label } = compressSync(trimmed);
|
|
160
|
+
originals.set(id, trimmed);
|
|
161
|
+
return label;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Disk failure: collapse newlines so the auto-submit we're
|
|
165
|
+
// preventing doesn't fire downstream.
|
|
166
|
+
return trimmed.replace(/\n/g, ' ');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
96
169
|
let installed = null;
|
|
97
170
|
/**
|
|
98
171
|
* Install the stdin pre-tap. Wraps `process.stdin.emit('data', …)`
|
|
@@ -103,72 +176,140 @@ let installed = null;
|
|
|
103
176
|
* MCP serve mode: never call this — `aiden mcp serve` doesn't run
|
|
104
177
|
* the REPL.
|
|
105
178
|
*/
|
|
106
|
-
function installPasteInterceptor(stdin) {
|
|
179
|
+
function installPasteInterceptor(stdin, opts = {}) {
|
|
107
180
|
if (installed)
|
|
108
181
|
return installed.restore;
|
|
182
|
+
const accumulationMs = opts.accumulationMs ?? ACCUMULATION_MS;
|
|
183
|
+
const watchdogMs = opts.watchdogMs ?? WATCHDOG_MS;
|
|
109
184
|
const origEmit = stdin.emit.bind(stdin);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
185
|
+
// State machine —
|
|
186
|
+
// normal : default; chunks pass through or accumulate
|
|
187
|
+
// in_marker_paste : between PASTE_BEGIN and PASTE_END; buf accumulates payload
|
|
188
|
+
let mode = 'normal';
|
|
189
|
+
let buf = '';
|
|
190
|
+
let markerTimer = null;
|
|
191
|
+
let pendingChunk = null;
|
|
192
|
+
let pendingTimer = null;
|
|
193
|
+
function emitDownstream(text) {
|
|
194
|
+
if (text.length === 0)
|
|
195
|
+
return;
|
|
196
|
+
origEmit('data', Buffer.from(text, 'utf8'));
|
|
197
|
+
}
|
|
198
|
+
function clearMarkerWatchdog() {
|
|
199
|
+
if (markerTimer) {
|
|
200
|
+
clearTimeout(markerTimer);
|
|
201
|
+
markerTimer = null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function armMarkerWatchdog() {
|
|
205
|
+
clearMarkerWatchdog();
|
|
206
|
+
markerTimer = setTimeout(() => {
|
|
207
|
+
// PASTE_END never arrived. Flush whatever we have and reset.
|
|
208
|
+
const payload = buf;
|
|
209
|
+
buf = '';
|
|
210
|
+
mode = 'normal';
|
|
211
|
+
markerTimer = null;
|
|
212
|
+
emitDownstream(payloadToEmission(payload));
|
|
213
|
+
}, watchdogMs);
|
|
214
|
+
}
|
|
215
|
+
function clearPending() {
|
|
216
|
+
if (pendingTimer) {
|
|
217
|
+
clearTimeout(pendingTimer);
|
|
218
|
+
pendingTimer = null;
|
|
219
|
+
}
|
|
220
|
+
pendingChunk = null;
|
|
221
|
+
}
|
|
222
|
+
function flushPendingAsIs() {
|
|
223
|
+
if (pendingChunk === null)
|
|
224
|
+
return;
|
|
225
|
+
const chunk = pendingChunk;
|
|
226
|
+
clearPending();
|
|
227
|
+
// Pending was a normal Enter — emit as-is, don't placeholder.
|
|
228
|
+
emitDownstream(chunk);
|
|
229
|
+
}
|
|
230
|
+
function flushPendingAsPaste() {
|
|
231
|
+
if (pendingChunk === null)
|
|
232
|
+
return;
|
|
233
|
+
const chunk = pendingChunk;
|
|
234
|
+
clearPending();
|
|
235
|
+
emitDownstream(payloadToEmission(chunk));
|
|
236
|
+
}
|
|
237
|
+
function processNormalised(text) {
|
|
113
238
|
let cursor = 0;
|
|
114
239
|
while (cursor < text.length) {
|
|
115
|
-
if (
|
|
240
|
+
if (mode === 'in_marker_paste') {
|
|
116
241
|
const endIdx = text.indexOf(PASTE_END, cursor);
|
|
117
242
|
if (endIdx === -1) {
|
|
118
|
-
|
|
243
|
+
buf += text.slice(cursor);
|
|
119
244
|
cursor = text.length;
|
|
245
|
+
// Watchdog stays armed — extending the buf without an end
|
|
246
|
+
// marker doesn't restart the clock; we still want to flush
|
|
247
|
+
// if the entire turn never produces PASTE_END.
|
|
120
248
|
}
|
|
121
249
|
else {
|
|
122
|
-
|
|
250
|
+
buf += text.slice(cursor, endIdx);
|
|
123
251
|
cursor = endIdx + PASTE_END.length;
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
// swallow the bytes pass through to readline, where they
|
|
127
|
-
// become an Enter event and auto-submit the prompt before
|
|
128
|
-
// the user has reviewed the paste. Eat at most one CR + one
|
|
129
|
-
// LF (in either order) right after PASTE_END.
|
|
130
|
-
if (text[cursor] === '\r')
|
|
131
|
-
cursor += 1;
|
|
252
|
+
// Swallow a trailing newline that some terminals emit
|
|
253
|
+
// immediately after PASTE_END.
|
|
132
254
|
if (text[cursor] === '\n')
|
|
133
255
|
cursor += 1;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (!trimmed.includes('\n') && trimmed.length <= 500) {
|
|
140
|
-
// Single-line, small — emit as-is so user can edit.
|
|
141
|
-
out += trimmed;
|
|
142
|
-
}
|
|
143
|
-
else {
|
|
144
|
-
// Multi-line or large — disk-back + emit label.
|
|
145
|
-
try {
|
|
146
|
-
const { id, label } = compressSync(trimmed);
|
|
147
|
-
originals.set(id, trimmed);
|
|
148
|
-
out += label;
|
|
149
|
-
}
|
|
150
|
-
catch {
|
|
151
|
-
// Disk failure: fall back to a single-space substitute
|
|
152
|
-
// so internal newlines don't trigger auto-submit.
|
|
153
|
-
out += trimmed.replace(/\n/g, ' ');
|
|
154
|
-
}
|
|
155
|
-
}
|
|
256
|
+
mode = 'normal';
|
|
257
|
+
clearMarkerWatchdog();
|
|
258
|
+
const payload = buf;
|
|
259
|
+
buf = '';
|
|
260
|
+
emitDownstream(payloadToEmission(payload));
|
|
156
261
|
}
|
|
262
|
+
continue;
|
|
157
263
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
264
|
+
// mode === 'normal'
|
|
265
|
+
const beginIdx = text.indexOf(PASTE_BEGIN, cursor);
|
|
266
|
+
if (beginIdx !== -1) {
|
|
267
|
+
// Pre-marker content: flush any pending and emit inline so
|
|
268
|
+
// it lands in inquirer's buffer ahead of the placeholder
|
|
269
|
+
// (preserves typed prefix when the user pastes after typing).
|
|
270
|
+
flushPendingAsIs();
|
|
271
|
+
if (beginIdx > cursor)
|
|
272
|
+
emitDownstream(text.slice(cursor, beginIdx));
|
|
273
|
+
cursor = beginIdx + PASTE_BEGIN.length;
|
|
274
|
+
mode = 'in_marker_paste';
|
|
275
|
+
armMarkerWatchdog();
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
// No marker in the remainder.
|
|
279
|
+
const remainder = text.slice(cursor);
|
|
280
|
+
cursor = text.length;
|
|
281
|
+
const nlCount = (remainder.match(/\n/g) ?? []).length;
|
|
282
|
+
const hasInternalNl = nlCount > 1 || (nlCount === 1 && !remainder.endsWith('\n'));
|
|
283
|
+
if (hasInternalNl) {
|
|
284
|
+
// Single bulk chunk with internal newlines — instant
|
|
285
|
+
// placeholder. Flush pending first so any prior single-line
|
|
286
|
+
// candidate isn't lost.
|
|
287
|
+
flushPendingAsIs();
|
|
288
|
+
emitDownstream(payloadToEmission(remainder));
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
// Candidate paste-line: non-empty content ending in `\n` with
|
|
292
|
+
// length > 1 (excludes bare Enter keystroke `"\n"`).
|
|
293
|
+
const isCandidate = remainder.endsWith('\n') && remainder.length > 1;
|
|
294
|
+
if (isCandidate) {
|
|
295
|
+
if (pendingChunk !== null) {
|
|
296
|
+
// Already pending — append, restart the window.
|
|
297
|
+
pendingChunk += remainder;
|
|
298
|
+
if (pendingTimer)
|
|
299
|
+
clearTimeout(pendingTimer);
|
|
300
|
+
pendingTimer = setTimeout(flushPendingAsPaste, accumulationMs);
|
|
163
301
|
}
|
|
164
302
|
else {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
state.inPaste = true;
|
|
303
|
+
pendingChunk = remainder;
|
|
304
|
+
pendingTimer = setTimeout(flushPendingAsIs, accumulationMs);
|
|
168
305
|
}
|
|
306
|
+
continue;
|
|
169
307
|
}
|
|
308
|
+
// Non-candidate (bare Enter, or non-`\n`-terminated keystroke).
|
|
309
|
+
// Flush pending first since this chunk closes the window.
|
|
310
|
+
flushPendingAsIs();
|
|
311
|
+
emitDownstream(remainder);
|
|
170
312
|
}
|
|
171
|
-
return out;
|
|
172
313
|
}
|
|
173
314
|
const wrappedEmit = function (event, ...args) {
|
|
174
315
|
if (event !== 'data')
|
|
@@ -176,19 +317,22 @@ function installPasteInterceptor(stdin) {
|
|
|
176
317
|
const chunk = args[0];
|
|
177
318
|
if (chunk == null)
|
|
178
319
|
return origEmit(event, ...args);
|
|
179
|
-
const
|
|
320
|
+
const raw = Buffer.isBuffer(chunk)
|
|
180
321
|
? chunk.toString('utf8')
|
|
181
322
|
: (typeof chunk === 'string' ? chunk : String(chunk));
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
323
|
+
const normalised = normalize(raw);
|
|
324
|
+
processNormalised(normalised);
|
|
325
|
+
// We always claim to have handled the emit. Downstream listeners
|
|
326
|
+
// fire from `emitDownstream` immediately on the same tick OR
|
|
327
|
+
// from a deferred timer in the accumulation case.
|
|
328
|
+
return true;
|
|
187
329
|
};
|
|
188
330
|
stdin.emit = wrappedEmit;
|
|
189
331
|
const restore = () => {
|
|
190
332
|
if (!installed)
|
|
191
333
|
return;
|
|
334
|
+
clearPending();
|
|
335
|
+
clearMarkerWatchdog();
|
|
192
336
|
stdin.emit = origEmit;
|
|
193
337
|
installed = null;
|
|
194
338
|
};
|