aiden-runtime 4.8.0 → 4.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -1
- package/dist/cli/v4/aidenCLI.js +35 -4
- package/dist/cli/v4/chatSession.js +43 -16
- package/dist/cli/v4/commands/daemon.js +47 -2
- package/dist/cli/v4/commands/daemonDoctor.js +212 -0
- package/dist/cli/v4/commands/daemonStatus.js +1 -1
- package/dist/cli/v4/commands/help.js +2 -0
- package/dist/cli/v4/commands/hooks.js +428 -0
- package/dist/cli/v4/commands/index.js +5 -1
- package/dist/cli/v4/commands/mcp.js +89 -1
- package/dist/cli/v4/commands/mcpClientInstall.js +359 -0
- package/dist/cli/v4/commands/memory.js +702 -0
- package/dist/cli/v4/commands/recovery.js +1 -1
- package/dist/cli/v4/commands/skin.js +7 -0
- package/dist/cli/v4/commands/theme.js +217 -0
- package/dist/cli/v4/commands/trigger.js +1 -1
- package/dist/cli/v4/commands/update.js +14 -2
- package/dist/cli/v4/design/tokens.js +52 -4
- package/dist/cli/v4/display.js +102 -46
- package/dist/cli/v4/pasteIntercept.js +214 -70
- package/dist/cli/v4/replyRenderer.js +145 -5
- package/dist/cli/v4/skinEngine.js +67 -0
- package/dist/core/v4/aidenAgent.js +45 -2
- package/dist/core/v4/daemon/api/runs.js +131 -0
- package/dist/core/v4/daemon/bootstrap.js +368 -13
- package/dist/core/v4/daemon/db/migrations.js +169 -0
- package/dist/core/v4/daemon/idempotency/runIdempotencyStore.js +128 -0
- package/dist/core/v4/daemon/incarnationStore.js +47 -0
- package/dist/core/v4/daemon/runs/attemptStore.js +67 -0
- package/dist/core/v4/daemon/runs/reclaim.js +88 -0
- package/dist/core/v4/daemon/runs/retryPolicy.js +73 -0
- package/dist/core/v4/daemon/runs/runWithRetry.js +80 -0
- package/dist/core/v4/daemon/runs/stuckAttemptWatchdog.js +72 -0
- package/dist/core/v4/daemon/spans/spanHelpers.js +272 -0
- package/dist/core/v4/daemon/spans/spanStore.js +113 -0
- package/dist/core/v4/daemon/triggerBus.js +50 -19
- package/dist/core/v4/hooks/auditQuery.js +67 -0
- package/dist/core/v4/hooks/dispatcher.js +286 -0
- package/dist/core/v4/hooks/index.js +46 -0
- package/dist/core/v4/hooks/lifecycle.js +27 -0
- package/dist/core/v4/hooks/manifest.js +142 -0
- package/dist/core/v4/hooks/registry.js +149 -0
- package/dist/core/v4/hooks/runtime/subprocessRunner.js +188 -0
- package/dist/core/v4/hooks/toolHookGate.js +76 -0
- package/dist/core/v4/hooks/trust.js +14 -0
- package/dist/core/v4/identity/contextManager.js +83 -0
- package/dist/core/v4/identity/daemonId.js +85 -0
- package/dist/core/v4/identity/enforcement.js +103 -0
- package/dist/core/v4/identity/executionContext.js +153 -0
- package/dist/core/v4/identity/hookExecution.js +62 -0
- package/dist/core/v4/identity/httpContext.js +68 -0
- package/dist/core/v4/identity/ids.js +185 -0
- package/dist/core/v4/identity/index.js +60 -0
- package/dist/core/v4/identity/subprocessContext.js +98 -0
- package/dist/core/v4/identity/traceparent.js +114 -0
- package/dist/core/v4/logger/index.js +3 -1
- package/dist/core/v4/logger/logger.js +28 -1
- package/dist/core/v4/logger/redact.js +149 -0
- package/dist/core/v4/logger/sinks/fileSink.js +13 -0
- package/dist/core/v4/logger/sinks/stdSink.js +19 -1
- package/dist/core/v4/mcp/install/backup.js +78 -0
- package/dist/core/v4/mcp/install/clientPaths.js +90 -0
- package/dist/core/v4/mcp/install/clients.js +203 -0
- package/dist/core/v4/mcp/install/healthCheck.js +83 -0
- package/dist/core/v4/mcp/install/jsoncMerge.js +174 -0
- package/dist/core/v4/mcp/install/profiles.js +109 -0
- package/dist/core/v4/mcp/install/wslDetect.js +62 -0
- package/dist/core/v4/memory/namespaceRegistry.js +117 -0
- package/dist/core/v4/memory/projectRoot.js +76 -0
- package/dist/core/v4/memory/reviewer/index.js +162 -0
- package/dist/core/v4/memory/reviewer/pendingStore.js +136 -0
- package/dist/core/v4/memory/reviewer/prompt.js +105 -0
- package/dist/core/v4/memory/reviewer/skipRules.js +92 -0
- package/dist/core/v4/memoryManager.js +57 -10
- package/dist/core/v4/paths.js +2 -0
- package/dist/core/v4/promptBuilder.js +6 -0
- package/dist/core/v4/subagent/spawnSubAgent.js +20 -7
- package/dist/core/v4/theme/bundledThemes.js +106 -0
- package/dist/core/v4/theme/themeLoader.js +160 -0
- package/dist/core/v4/theme/themeRegistry.js +97 -0
- package/dist/core/v4/theme/themeWatcher.js +95 -0
- package/dist/core/v4/toolRegistry.js +71 -8
- 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 +4 -0
- package/dist/moat/memoryGuard.js +8 -1
- package/dist/providers/v4/anthropicAdapter.js +10 -4
- package/dist/tools/v4/backends/local.js +19 -2
- package/dist/tools/v4/sessions/recallSession.js +6 -1
- package/package.json +3 -3
- package/plugins/aiden-plugin-chatgpt-plus/index.js +10 -1
- package/themes/default.yaml +52 -0
- package/themes/dracula.yaml +32 -0
- package/themes/light.yaml +32 -0
- package/themes/monochrome.yaml +31 -0
- package/themes/tokyo-night.yaml +32 -0
- package/dist/core/pluginSystem.js +0 -121
- package/dist/tools/v4/ui/_uiSmokeTool.js +0 -60
|
@@ -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
|
};
|
|
@@ -39,6 +39,7 @@ const tokens_1 = require("./design/tokens");
|
|
|
39
39
|
// callsites in this file with `getBodyWidth()` and adds soft-wrap for
|
|
40
40
|
// code-block lines that previously overflowed the viewport.
|
|
41
41
|
const frame_1 = require("./display/frame");
|
|
42
|
+
const box_1 = require("./box");
|
|
42
43
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
43
44
|
const TerminalRenderer = require('marked-terminal').default ?? require('marked-terminal');
|
|
44
45
|
function paint(kind) {
|
|
@@ -182,7 +183,7 @@ function renderBlockquote(quote) {
|
|
|
182
183
|
return quote
|
|
183
184
|
.split('\n')
|
|
184
185
|
.map((ln) => (ln.length === 0 ? rail.trimEnd() : `${rail}${ln}`))
|
|
185
|
-
.join('\n') + '\n';
|
|
186
|
+
.join('\n') + '\n\n'; // v4.9.0 pre-ship UI: blank line after blockquote
|
|
186
187
|
}
|
|
187
188
|
/**
|
|
188
189
|
* v4.1.3-essentials reply-polish: 4-tier heading hierarchy using the
|
|
@@ -632,11 +633,150 @@ function getReplyRenderer() {
|
|
|
632
633
|
}
|
|
633
634
|
}
|
|
634
635
|
proto._listDepth -= 1;
|
|
635
|
-
//
|
|
636
|
-
//
|
|
637
|
-
//
|
|
636
|
+
// v4.9.0 pre-ship UI: top-level list closes with a BLANK LINE
|
|
637
|
+
// (`\n\n`) so a following paragraph / heading / table reads with
|
|
638
|
+
// breathing room. Nested lists stay tight (`\n`) so they nest
|
|
639
|
+
// cleanly under their parent item.
|
|
638
640
|
const out = lines.join('\n');
|
|
639
|
-
return proto._listDepth === 0 ? out + '\n' : out + '\n';
|
|
641
|
+
return proto._listDepth === 0 ? out + '\n\n' : out + '\n';
|
|
642
|
+
};
|
|
643
|
+
// ── v4.8.1 Slice 2 — markdown table override ──────────────────────────
|
|
644
|
+
//
|
|
645
|
+
// Why: marked-terminal's default table renderer (cli-table3) auto-
|
|
646
|
+
// wraps cells but doesn't keep wrap-continuation lines aligned to
|
|
647
|
+
// the original row — wide tables with 5+ columns fragment into
|
|
648
|
+
// vertical pipe rails that don't read as rows. The narrow 2-col
|
|
649
|
+
// tables that smoke-tested fine were within the no-wrap budget.
|
|
650
|
+
//
|
|
651
|
+
// Strategy: own the entire render from the marked v15 token object.
|
|
652
|
+
// Use `parser.parseInline(cell.tokens)` to get ANSI-painted cell
|
|
653
|
+
// text, then proportionally distribute the terminal-width budget
|
|
654
|
+
// across columns (clamping to natural max width), wrap each cell
|
|
655
|
+
// to its column width, and render the box with the same row
|
|
656
|
+
// height for every cell in the row so visual rows stay tight.
|
|
657
|
+
//
|
|
658
|
+
// Token-source the box chars from `glyphs.chrome.*` so a single
|
|
659
|
+
// glyph swap propagates here automatically (consistent with the
|
|
660
|
+
// rest of v4.8.x chrome).
|
|
661
|
+
renderer.table = function (header, body) {
|
|
662
|
+
// marked v15 token: { header: [cellTok], rows: [[cellTok]] }.
|
|
663
|
+
// Older string-based API: (headerHtml, bodyHtml) — we fall back
|
|
664
|
+
// to a naive concatenation so the reply isn't lost entirely.
|
|
665
|
+
if (typeof header !== 'object' || header === null) {
|
|
666
|
+
return String(header ?? '') + (body !== undefined ? String(body) : '') + '\n';
|
|
667
|
+
}
|
|
668
|
+
const tok = header;
|
|
669
|
+
const parser = this.parser;
|
|
670
|
+
const renderCell = (c) => {
|
|
671
|
+
if (c.tokens && parser?.parseInline) {
|
|
672
|
+
try {
|
|
673
|
+
return parser.parseInline(c.tokens).trim();
|
|
674
|
+
}
|
|
675
|
+
catch {
|
|
676
|
+
return (c.text ?? '').trim();
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return (c.text ?? '').trim();
|
|
680
|
+
};
|
|
681
|
+
const headers = (tok.header ?? []).map(renderCell);
|
|
682
|
+
const rows = (tok.rows ?? []).map((r) => r.map(renderCell));
|
|
683
|
+
const cols = headers.length;
|
|
684
|
+
if (cols === 0)
|
|
685
|
+
return '';
|
|
686
|
+
// Layout budget. Reply chrome family lives at col 2.
|
|
687
|
+
const indent = ' ';
|
|
688
|
+
const termCols = process.stdout.columns ?? 100;
|
|
689
|
+
const innerBudget = Math.max(40, Math.min(termCols, 110) - indent.length);
|
|
690
|
+
// Chrome per row = `│ ` (2) per col + trailing `│` (1) + 1 trailing
|
|
691
|
+
// space per cell already absorbed in the budget below.
|
|
692
|
+
const chromeCost = 3 * cols + 1;
|
|
693
|
+
const contentBudget = Math.max(cols * 4, innerBudget - chromeCost);
|
|
694
|
+
// Natural width = max(header, body) visible width per column.
|
|
695
|
+
const naturalW = headers.map((h, i) => {
|
|
696
|
+
const hw = (0, box_1.visibleLength)(h);
|
|
697
|
+
const cw = rows.reduce((m, r) => Math.max(m, (0, box_1.visibleLength)(r[i] ?? '')), 0);
|
|
698
|
+
return Math.max(hw, cw, 1);
|
|
699
|
+
});
|
|
700
|
+
// v4.8.1 Slice 2 hotfix #2 — header-floor + proportional allocation.
|
|
701
|
+
//
|
|
702
|
+
// Each column's minimum is `max(headerWidth, MIN_COL_W)` so column
|
|
703
|
+
// headers NEVER wrap — they are the column identifier; wrapping
|
|
704
|
+
// them ("Framework" → "Framew/ork") fragments scanability worse
|
|
705
|
+
// than wrapping body cells. Body content above the header width
|
|
706
|
+
// is what gets compressed under width pressure.
|
|
707
|
+
//
|
|
708
|
+
// Algorithm:
|
|
709
|
+
// 1. Compute `minPerCol = max(headerW[i], MIN_COL_W)` per column.
|
|
710
|
+
// 2. If sum(minPerCol) >= contentBudget (very narrow terminal),
|
|
711
|
+
// use minPerCol as-is — body cells will wrap to fit, headers
|
|
712
|
+
// stay intact.
|
|
713
|
+
// 3. Else if sum(naturalW) <= contentBudget, use natural widths
|
|
714
|
+
// (no wrap needed anywhere).
|
|
715
|
+
// 4. Else: floor at minPerCol, distribute remaining budget
|
|
716
|
+
// proportionally to each column's "extra need above min",
|
|
717
|
+
// then hand rounding leftover to widest-natural cols first.
|
|
718
|
+
const MIN_COL_W = 4;
|
|
719
|
+
const headerW = headers.map((h) => (0, box_1.visibleLength)(h));
|
|
720
|
+
const minPerCol = naturalW.map((_, i) => Math.max(headerW[i], MIN_COL_W));
|
|
721
|
+
const totalMin = minPerCol.reduce((a, b) => a + b, 0);
|
|
722
|
+
const totalNatW = naturalW.reduce((a, b) => a + b, 0);
|
|
723
|
+
let colWidths;
|
|
724
|
+
if (totalMin >= contentBudget) {
|
|
725
|
+
colWidths = minPerCol.slice();
|
|
726
|
+
}
|
|
727
|
+
else if (totalNatW <= contentBudget) {
|
|
728
|
+
colWidths = naturalW.slice();
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
colWidths = minPerCol.slice();
|
|
732
|
+
const extraNeed = naturalW.map((w, i) => Math.max(0, w - minPerCol[i]));
|
|
733
|
+
const totalNeed = extraNeed.reduce((a, b) => a + b, 0);
|
|
734
|
+
const pool = contentBudget - totalMin;
|
|
735
|
+
if (totalNeed > 0) {
|
|
736
|
+
for (let i = 0; i < cols; i += 1) {
|
|
737
|
+
colWidths[i] += Math.floor((extraNeed[i] * pool) / totalNeed);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
let leftover = contentBudget - colWidths.reduce((a, b) => a + b, 0);
|
|
741
|
+
const order = naturalW.map((_, i) => i).sort((a, b) => naturalW[b] - naturalW[a]);
|
|
742
|
+
for (let k = 0; leftover > 0 && k < cols * 2; k += 1) {
|
|
743
|
+
const idx = order[k % cols];
|
|
744
|
+
if (colWidths[idx] < naturalW[idx]) {
|
|
745
|
+
colWidths[idx] += 1;
|
|
746
|
+
leftover -= 1;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
// ANSI-aware cell wrap. frameWrap handles colour-code-aware width.
|
|
751
|
+
const wrapCell = (text, w) => w <= 0 ? [''] : (0, frame_1.wrap)(text, w, { trim: false, hard: true }).split('\n');
|
|
752
|
+
const sk = (0, skinEngine_1.getSkinEngine)();
|
|
753
|
+
const ch = tokens_1.glyphs.chrome;
|
|
754
|
+
const rule = (l, m, r) => indent + sk.applyColors(l + colWidths.map((w) => ch.hLine.repeat(w + 2)).join(m) + r, 'muted');
|
|
755
|
+
const vBar = sk.applyColors(ch.vLine, 'muted');
|
|
756
|
+
const renderRow = (cells) => {
|
|
757
|
+
const height = Math.max(...cells.map((c) => c.length), 1);
|
|
758
|
+
const out = [];
|
|
759
|
+
for (let line = 0; line < height; line += 1) {
|
|
760
|
+
const cellLines = cells.map((cellLines2, ci) => {
|
|
761
|
+
const cellLine = cellLines2[line] ?? '';
|
|
762
|
+
const pad = Math.max(0, colWidths[ci] - (0, box_1.visibleLength)(cellLine));
|
|
763
|
+
return ' ' + cellLine + ' '.repeat(pad) + ' ';
|
|
764
|
+
});
|
|
765
|
+
out.push(indent + vBar + cellLines.join(vBar) + vBar);
|
|
766
|
+
}
|
|
767
|
+
return out.join('\n');
|
|
768
|
+
};
|
|
769
|
+
const wrappedHeader = headers.map((h, i) => wrapCell(h, colWidths[i]));
|
|
770
|
+
const wrappedRows = rows.map((r) => r.map((c, i) => wrapCell(c, colWidths[i])));
|
|
771
|
+
const lines = [rule(ch.topLeft, ch.teeDown, ch.topRight)];
|
|
772
|
+
if (headers.length > 0) {
|
|
773
|
+
lines.push(renderRow(wrappedHeader));
|
|
774
|
+
lines.push(rule(ch.teeRight, ch.cross, ch.teeLeft));
|
|
775
|
+
}
|
|
776
|
+
for (const row of wrappedRows)
|
|
777
|
+
lines.push(renderRow(row));
|
|
778
|
+
lines.push(rule(ch.botLeft, ch.teeUp, ch.botRight));
|
|
779
|
+
return lines.join('\n') + '\n';
|
|
640
780
|
};
|
|
641
781
|
cachedRenderer = {
|
|
642
782
|
render(text) {
|
|
@@ -32,10 +32,58 @@ const node_fs_1 = require("node:fs");
|
|
|
32
32
|
const node_path_1 = __importDefault(require("node:path"));
|
|
33
33
|
const node_os_1 = __importDefault(require("node:os"));
|
|
34
34
|
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
35
|
+
// v4.9.0 Slice 1a hotfix — read live theme overrides from tokens.ts.
|
|
36
|
+
const tokens_1 = require("./design/tokens");
|
|
37
|
+
const themeRegistry_1 = require("../../core/v4/theme/themeRegistry");
|
|
35
38
|
/** Wrap text with a 24-bit ANSI foreground colour. */
|
|
36
39
|
function ansiRgb(text, r, g, b) {
|
|
37
40
|
return `\x1b[38;2;${r};${g};${b}m${text}\x1b[39m`;
|
|
38
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* v4.9.0 Slice 1a hotfix — map each SkinEngine `ColorKind` to a dotted
|
|
44
|
+
* path inside the v4.8 `tokens.ts` colour tree so the legacy paint API
|
|
45
|
+
* (`applyColors(text, kind)`) can resolve user-theme overrides without
|
|
46
|
+
* touching the legacy skin YAML cache. Each kind picks the closest
|
|
47
|
+
* semantic equivalent from the new tree; kinds without a natural fit
|
|
48
|
+
* (e.g. `agent`, `user`) fall through to the skin's own RGB tuple.
|
|
49
|
+
*
|
|
50
|
+
* The lookup is only consulted when a user theme is active (i.e.
|
|
51
|
+
* `getActiveThemePath() !== null`). When no theme is loaded, the
|
|
52
|
+
* legacy skin path runs unchanged — preserves /skin custom-palette
|
|
53
|
+
* users from being silently overridden by tokens.ts baselines.
|
|
54
|
+
*/
|
|
55
|
+
const COLOR_KIND_TO_TOKEN_PATH = {
|
|
56
|
+
brand: 'brand.primary',
|
|
57
|
+
accent: 'brand.primary',
|
|
58
|
+
heading: 'brand.primary',
|
|
59
|
+
tool: 'metrics.model',
|
|
60
|
+
session: 'metrics.model',
|
|
61
|
+
error: 'semantic.error',
|
|
62
|
+
warn: 'semantic.warn',
|
|
63
|
+
success: 'semantic.success',
|
|
64
|
+
muted: 'content.secondary',
|
|
65
|
+
tertiary: 'content.tertiary',
|
|
66
|
+
metric_turn: 'metrics.turnCount',
|
|
67
|
+
degraded: 'semantic.warn',
|
|
68
|
+
};
|
|
69
|
+
function hexToRgb(hex) {
|
|
70
|
+
const m3 = /^#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])$/.exec(hex);
|
|
71
|
+
if (m3)
|
|
72
|
+
return [parseInt(m3[1] + m3[1], 16), parseInt(m3[2] + m3[2], 16), parseInt(m3[3] + m3[3], 16)];
|
|
73
|
+
const m6 = /^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/.exec(hex);
|
|
74
|
+
if (m6)
|
|
75
|
+
return [parseInt(m6[1], 16), parseInt(m6[2], 16), parseInt(m6[3], 16)];
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
function readDottedPath(root, dotted) {
|
|
79
|
+
let node = root;
|
|
80
|
+
for (const seg of dotted.split('.')) {
|
|
81
|
+
if (node === null || typeof node !== 'object')
|
|
82
|
+
return undefined;
|
|
83
|
+
node = node[seg];
|
|
84
|
+
}
|
|
85
|
+
return node;
|
|
86
|
+
}
|
|
39
87
|
const BRAND_ORANGE = [0xff, 0x6b, 0x35];
|
|
40
88
|
const DEFAULT_SKIN = {
|
|
41
89
|
name: 'default',
|
|
@@ -250,6 +298,25 @@ class SkinEngine {
|
|
|
250
298
|
applyColors(text, kind) {
|
|
251
299
|
if (this.forceMono)
|
|
252
300
|
return text;
|
|
301
|
+
// v4.9.0 Slice 1a hotfix — when a user theme is active, resolve
|
|
302
|
+
// the colour from the live tokens.ts tree FIRST. This lets a
|
|
303
|
+
// ~/.aiden/theme.yaml override every paint surface that routes
|
|
304
|
+
// through SkinEngine (Aiden reply chrome, panel bars, status
|
|
305
|
+
// footer text, tool rows) without requiring users to also
|
|
306
|
+
// re-author a parallel ~/.aiden/skins/<name>.yaml. When no user
|
|
307
|
+
// theme is active, the legacy skin RGB path runs unchanged —
|
|
308
|
+
// preserves /skin custom-palette users from regression.
|
|
309
|
+
if ((0, themeRegistry_1.getActivePath)() !== null) {
|
|
310
|
+
const dotted = COLOR_KIND_TO_TOKEN_PATH[kind];
|
|
311
|
+
if (dotted) {
|
|
312
|
+
const hex = readDottedPath(tokens_1.colors, dotted);
|
|
313
|
+
if (typeof hex === 'string') {
|
|
314
|
+
const rgb = hexToRgb(hex);
|
|
315
|
+
if (rgb)
|
|
316
|
+
return ansiRgb(text, rgb[0], rgb[1], rgb[2]);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
253
320
|
const rgb = this.current.colors[kind];
|
|
254
321
|
if (!rgb)
|
|
255
322
|
return text;
|