mixdog 0.9.22 → 0.9.24
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 +1 -4
- package/package.json +1 -1
- package/scripts/boot-smoke.mjs +1 -1
- package/scripts/channel-daemon-smoke.mjs +483 -0
- package/scripts/channel-daemon-stub.mjs +80 -0
- package/scripts/debounced-skills-async-save-test.mjs +57 -0
- package/scripts/explore-bench-tmp.mjs +17 -0
- package/scripts/find-fuzzy-hidden-test.mjs +145 -0
- package/scripts/mcp-grace-deferred-test.mjs +149 -0
- package/scripts/statusline-quota-hysteresis-test.mjs +37 -0
- package/scripts/tool-smoke.mjs +68 -47
- package/src/rules/agent/30-explorer.md +6 -0
- package/src/rules/shared/01-tool.md +11 -4
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +76 -1
- package/src/runtime/agent/orchestrator/config.mjs +33 -7
- package/src/runtime/agent/orchestrator/context/collect.mjs +43 -8
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +226 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +184 -33
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +38 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +39 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +5 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +24 -7
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +6 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +37 -4
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +11 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +14 -3
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +40 -6
- package/src/runtime/agent/orchestrator/session/store.mjs +30 -14
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +13 -5
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +62 -24
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -6
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +20 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +34 -3
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +220 -27
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +61 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +67 -27
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +6 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +54 -5
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +97 -54
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +49 -31
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +35 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +126 -22
- package/src/runtime/channels/lib/crash-log.mjs +4 -2
- package/src/runtime/channels/lib/output-forwarder.mjs +2 -1
- package/src/runtime/channels/lib/owned-runtime.mjs +67 -223
- package/src/runtime/channels/lib/owner-heartbeat.mjs +9 -62
- package/src/runtime/channels/lib/parent-bridge.mjs +16 -1
- package/src/runtime/channels/lib/runtime-paths.mjs +32 -113
- package/src/runtime/channels/lib/session-discovery.mjs +2 -1
- package/src/runtime/channels/lib/tool-dispatch.mjs +22 -14
- package/src/runtime/channels/lib/tool-format.mjs +7 -2
- package/src/runtime/channels/lib/worker-main.mjs +42 -30
- package/src/runtime/memory/index.mjs +54 -7
- package/src/runtime/memory/lib/embedding-warmup.mjs +7 -0
- package/src/runtime/memory/lib/pg/process.mjs +85 -40
- package/src/runtime/memory/lib/pg/supervisor.mjs +82 -17
- package/src/runtime/memory/lib/query-handlers.mjs +4 -1
- package/src/runtime/memory/lib/recall-format.mjs +7 -3
- package/src/runtime/memory/tool-defs.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +150 -6
- package/src/runtime/shared/background-tasks.mjs +1 -1
- package/src/runtime/shared/config.mjs +53 -1
- package/src/runtime/shared/tool-execution-contract.mjs +1 -1
- package/src/runtime/shared/tool-primitives.mjs +31 -1
- package/src/runtime/shared/tool-surface.mjs +42 -13
- package/src/runtime/shared/update-checker.mjs +3 -0
- package/src/runtime/shared/user-data-guard.mjs +66 -0
- package/src/session-runtime/config-lifecycle.mjs +221 -20
- package/src/session-runtime/lifecycle-api.mjs +9 -0
- package/src/session-runtime/mcp-glue.mjs +93 -1
- package/src/session-runtime/resource-api.mjs +62 -8
- package/src/session-runtime/runtime-core.mjs +118 -4
- package/src/session-runtime/session-text.mjs +41 -0
- package/src/session-runtime/session-turn-api.mjs +50 -0
- package/src/session-runtime/settings-api.mjs +8 -1
- package/src/session-runtime/tool-catalog.mjs +350 -38
- package/src/session-runtime/tool-defs.mjs +7 -7
- package/src/session-runtime/workflow.mjs +2 -1
- package/src/standalone/channel-admin.mjs +32 -3
- package/src/standalone/channel-daemon-client.mjs +226 -0
- package/src/standalone/channel-daemon-transport.mjs +545 -0
- package/src/standalone/channel-daemon.mjs +176 -0
- package/src/standalone/channel-worker.mjs +224 -4
- package/src/standalone/explore-tool.mjs +87 -15
- package/src/standalone/hook-bus.mjs +71 -3
- package/src/tui/App.jsx +107 -19
- package/src/tui/app/clipboard.mjs +39 -19
- package/src/tui/app/doctor.mjs +57 -0
- package/src/tui/app/extension-pickers.mjs +53 -9
- package/src/tui/app/maintenance-pickers.mjs +0 -5
- package/src/tui/app/slash-dispatch.mjs +4 -4
- package/src/tui/app/text-layout.mjs +11 -0
- package/src/tui/app/use-mouse-input.mjs +235 -51
- package/src/tui/app/use-prompt-handlers.mjs +49 -30
- package/src/tui/app/use-transcript-scroll.mjs +124 -27
- package/src/tui/app/use-transcript-window.mjs +55 -1
- package/src/tui/components/Message.jsx +1 -1
- package/src/tui/components/PromptInput.jsx +3 -1
- package/src/tui/components/QueuedCommands.jsx +21 -10
- package/src/tui/components/StatusLine.jsx +3 -3
- package/src/tui/components/ToolExecution.jsx +16 -4
- package/src/tui/components/TranscriptItem.jsx +1 -1
- package/src/tui/dist/index.mjs +820 -326
- package/src/tui/engine/agent-job-feed.mjs +5 -0
- package/src/tui/engine/notification-plan.mjs +5 -0
- package/src/tui/engine/session-api.mjs +23 -8
- package/src/tui/engine/session-flow.mjs +6 -0
- package/src/tui/engine/tool-card-results.mjs +14 -5
- package/src/tui/engine/turn.mjs +9 -2
- package/src/tui/engine.mjs +32 -5
- package/src/tui/index.jsx +62 -18
- package/src/tui/paste-attachments.mjs +26 -0
- package/src/ui/statusline-agents.mjs +36 -0
- package/src/ui/statusline.mjs +60 -10
- package/src/ui/tool-card.mjs +8 -1
- package/src/vendor/statusline/bin/statusline-route.mjs +23 -7
- package/src/workflows/bench/WORKFLOW.md +46 -0
- package/src/workflows/default/WORKFLOW.md +6 -0
- package/src/workflows/solo/WORKFLOW.md +5 -0
- package/vendor/ink/build/ink.js +23 -1
- package/vendor/ink/build/output.js +154 -71
- package/vendor/ink/build/render-node-to-output.js +44 -2
- package/vendor/ink/build/render.js +4 -0
- package/vendor/ink/build/renderer.js +4 -1
|
@@ -84,30 +84,86 @@ export function useTranscriptScroll({
|
|
|
84
84
|
const scroll = stitchHarvestScrollRef.current;
|
|
85
85
|
for (const row of rows) {
|
|
86
86
|
if (!row || typeof row.y !== 'number') continue;
|
|
87
|
-
|
|
87
|
+
// Store text AND the soft-wrap continuation flag so the stitch join can
|
|
88
|
+
// rejoin word-wrapped rows into their logical line (mirrors output.js).
|
|
89
|
+
stitchBufferRef.current.set(row.y - scroll, {
|
|
90
|
+
text: typeof row.text === 'string' ? row.text : '',
|
|
91
|
+
sw: row.sw === true,
|
|
92
|
+
});
|
|
88
93
|
}
|
|
89
94
|
}, 0);
|
|
90
95
|
}, [store]);
|
|
91
96
|
|
|
97
|
+
// Synchronous sibling of harvestStitchRowsSoon: snapshot the rows CURRENTLY
|
|
98
|
+
// under the selection into the stitch buffer immediately, keyed by the given
|
|
99
|
+
// (pre-scroll) offset. Called right before a scroll shifts those rows out of
|
|
100
|
+
// view — mirrors selection.ts captureScrolledRows, which grabs the outgoing
|
|
101
|
+
// rows BEFORE scrollBy overwrites them. The deferred harvest could never see
|
|
102
|
+
// rows that a fast drag/wheel scrolled past between paint and its setTimeout.
|
|
103
|
+
// selectionRows is harvested by the renderer UNCONDITIONALLY (even on the
|
|
104
|
+
// captureText:false motion paints, output.js), so this works mid-drag.
|
|
105
|
+
const harvestStitchRowsNow = useCallback((scroll) => {
|
|
106
|
+
if (dragRef.current.region !== 'transcript') return;
|
|
107
|
+
const rows = store.getRenderSelectionRows?.();
|
|
108
|
+
if (!Array.isArray(rows)) return;
|
|
109
|
+
const s = Number(scroll) || 0;
|
|
110
|
+
for (const row of rows) {
|
|
111
|
+
if (!row || typeof row.y !== 'number') continue;
|
|
112
|
+
stitchBufferRef.current.set(row.y - s, {
|
|
113
|
+
text: typeof row.text === 'string' ? row.text : '',
|
|
114
|
+
sw: row.sw === true,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}, [store]);
|
|
118
|
+
|
|
92
119
|
// Map the CURRENT rect + current scrollTarget onto the content-key range and
|
|
93
|
-
// join buffered rows sorted by key with '\n'
|
|
94
|
-
//
|
|
120
|
+
// join buffered rows sorted by key with '\n'. Returns { text, complete }:
|
|
121
|
+
// `complete` is true only when the harvested keys covering the selection form
|
|
122
|
+
// a CONTIGUOUS run (no interior gap). The old code silently skipped missing
|
|
123
|
+
// keys and returned only the string, so a gap (a scrolled-off row that was
|
|
124
|
+
// never harvested) produced a stitched copy with a line dropped in the middle
|
|
125
|
+
// — a mangled, shorter-than-real result the caller then preferred purely on
|
|
126
|
+
// length. Callers must gate on `complete` before preferring the stitch.
|
|
127
|
+
// Returns { text: '', complete: false } when unusable so callers fall back to
|
|
128
|
+
// render/remembered text.
|
|
95
129
|
const getStitchedSelectionText = useCallback(() => {
|
|
130
|
+
const empty = { text: '', complete: false };
|
|
96
131
|
const buf = stitchBufferRef.current;
|
|
97
|
-
if (!buf.size) return
|
|
98
|
-
if (dragRef.current.region !== 'transcript') return
|
|
132
|
+
if (!buf.size) return empty;
|
|
133
|
+
if (dragRef.current.region !== 'transcript') return empty;
|
|
99
134
|
const rect = dragRef.current.rect;
|
|
100
|
-
if (!rect) return
|
|
135
|
+
if (!rect) return empty;
|
|
101
136
|
const y1 = Number(rect.y1);
|
|
102
137
|
const y2 = Number(rect.y2);
|
|
103
|
-
if (!Number.isFinite(y1) || !Number.isFinite(y2)) return
|
|
138
|
+
if (!Number.isFinite(y1) || !Number.isFinite(y2)) return empty;
|
|
104
139
|
const scroll = Number(scrollTargetRef.current) || 0;
|
|
105
140
|
const lo = Math.min(y1, y2) - scroll;
|
|
106
141
|
const hi = Math.max(y1, y2) - scroll;
|
|
107
142
|
const keys = [...buf.keys()].filter((k) => k >= lo && k <= hi).sort((a, b) => a - b);
|
|
108
|
-
if (!keys.length) return
|
|
109
|
-
|
|
110
|
-
|
|
143
|
+
if (!keys.length) return empty;
|
|
144
|
+
// FULL coverage of the selection's [lo..hi] row range with no hole. Keys are
|
|
145
|
+
// already filtered to [lo..hi] and unique, so a count equal to the range
|
|
146
|
+
// size means every selected row is present (interior AND both endpoints).
|
|
147
|
+
// Internal contiguity alone was not enough: an endpoint-missing stitch (e.g.
|
|
148
|
+
// the top/bottom selected row never harvested) is internally contiguous yet
|
|
149
|
+
// drops a boundary line, so it must NOT be marked complete and win in copy.
|
|
150
|
+
const complete = keys.length === hi - lo + 1;
|
|
151
|
+
// SOFT-WRAP JOIN (same rule as output.js getSelectedText): a row whose sw
|
|
152
|
+
// flag is set is a word-wrap continuation — concatenate it onto the prior
|
|
153
|
+
// logical line WITHOUT a newline; only source/hard breaks emit '\n'. Blank
|
|
154
|
+
// inner rows ('' text) survive as empty logical lines (paragraph gaps).
|
|
155
|
+
// Trailing whitespace is trimmed once per logical-line end.
|
|
156
|
+
const logical = [];
|
|
157
|
+
for (const k of keys) {
|
|
158
|
+
const entry = buf.get(k);
|
|
159
|
+
if (entry == null) continue;
|
|
160
|
+
const t = typeof entry === 'string' ? entry : (entry.text ?? '');
|
|
161
|
+
const sw = typeof entry === 'string' ? false : entry.sw === true;
|
|
162
|
+
if (sw && logical.length > 0) logical[logical.length - 1] += t;
|
|
163
|
+
else logical.push(t);
|
|
164
|
+
}
|
|
165
|
+
const text = logical.map((l) => l.replace(/\s+$/u, '')).join('\n');
|
|
166
|
+
return text.trim() ? { text, complete } : empty;
|
|
111
167
|
}, []);
|
|
112
168
|
|
|
113
169
|
const stopSmoothScroll = useCallback(() => {
|
|
@@ -221,6 +277,36 @@ export function useTranscriptScroll({
|
|
|
221
277
|
return true;
|
|
222
278
|
}, [store, rememberSelectionTextSoon, harvestStitchRowsSoon]);
|
|
223
279
|
|
|
280
|
+
// Shared guard for EVERY direct (non-throttled) paint path: a pending
|
|
281
|
+
// throttled repaint (state.timer/state.pending, armed by
|
|
282
|
+
// applySelectionRectThrottled) would fire AFTER a direct paint and stamp a
|
|
283
|
+
// stale pre-scroll/pre-direction rect over the current one — surfacing as two
|
|
284
|
+
// coexisting highlights. Cancel it before any direct paint.
|
|
285
|
+
const cancelPendingSelectionPaint = useCallback(() => {
|
|
286
|
+
const state = selectionPaintRef.current;
|
|
287
|
+
if (state.timer) {
|
|
288
|
+
clearTimeout(state.timer);
|
|
289
|
+
state.timer = null;
|
|
290
|
+
}
|
|
291
|
+
state.pending = null;
|
|
292
|
+
}, []);
|
|
293
|
+
|
|
294
|
+
// Commit an armed-but-unpainted throttled rect NOW, so paths that read the
|
|
295
|
+
// rendered selection (the pre-scroll stitch harvest) see the newest fast-drag
|
|
296
|
+
// rect rather than the previous rendered one. Cancel-only would drop the
|
|
297
|
+
// pending rect and lose rows it covered that scroll off before the rebuild.
|
|
298
|
+
const flushPendingSelectionPaint = useCallback(() => {
|
|
299
|
+
const state = selectionPaintRef.current;
|
|
300
|
+
if (!state.timer && !state.pending) return;
|
|
301
|
+
const pending = state.pending;
|
|
302
|
+
if (state.timer) {
|
|
303
|
+
clearTimeout(state.timer);
|
|
304
|
+
state.timer = null;
|
|
305
|
+
}
|
|
306
|
+
state.pending = null;
|
|
307
|
+
if (pending) paintSelectionRect(pending, { rememberText: false, immediate: true });
|
|
308
|
+
}, [paintSelectionRect]);
|
|
309
|
+
|
|
224
310
|
const applySelectionRect = useCallback((rect) => {
|
|
225
311
|
const clippedRect = withSelectionClip(rect);
|
|
226
312
|
dragRef.current.rect = clippedRect || null;
|
|
@@ -228,14 +314,9 @@ export function useTranscriptScroll({
|
|
|
228
314
|
selectionTextRef.current = '';
|
|
229
315
|
clearStitchBuffer();
|
|
230
316
|
}
|
|
231
|
-
|
|
232
|
-
if (state.timer) {
|
|
233
|
-
clearTimeout(state.timer);
|
|
234
|
-
state.timer = null;
|
|
235
|
-
state.pending = null;
|
|
236
|
-
}
|
|
317
|
+
cancelPendingSelectionPaint();
|
|
237
318
|
paintSelectionRect(clippedRect, { rememberText: true, immediate: true });
|
|
238
|
-
}, [paintSelectionRect, withSelectionClip, clearStitchBuffer]);
|
|
319
|
+
}, [paintSelectionRect, withSelectionClip, clearStitchBuffer, cancelPendingSelectionPaint]);
|
|
239
320
|
|
|
240
321
|
const applySelectionRectThrottled = useCallback((rect) => {
|
|
241
322
|
const clippedRect = withSelectionClip(rect, { captureText: false });
|
|
@@ -246,11 +327,7 @@ export function useTranscriptScroll({
|
|
|
246
327
|
const now = Date.now();
|
|
247
328
|
const elapsed = now - state.t;
|
|
248
329
|
if (elapsed >= SELECTION_PAINT_INTERVAL_MS) {
|
|
249
|
-
|
|
250
|
-
clearTimeout(state.timer);
|
|
251
|
-
state.timer = null;
|
|
252
|
-
state.pending = null;
|
|
253
|
-
}
|
|
330
|
+
cancelPendingSelectionPaint();
|
|
254
331
|
paintSelectionRect(clippedRect, { rememberText: false });
|
|
255
332
|
return;
|
|
256
333
|
}
|
|
@@ -265,7 +342,7 @@ export function useTranscriptScroll({
|
|
|
265
342
|
}, Math.max(1, SELECTION_PAINT_INTERVAL_MS - elapsed));
|
|
266
343
|
state.timer.unref?.();
|
|
267
344
|
}
|
|
268
|
-
}, [paintSelectionRect, withSelectionClip]);
|
|
345
|
+
}, [paintSelectionRect, withSelectionClip, cancelPendingSelectionPaint]);
|
|
269
346
|
|
|
270
347
|
const selectionPointAtCurrentScroll = useCallback((point, pointScroll = 0) => {
|
|
271
348
|
if (!point) return null;
|
|
@@ -298,13 +375,13 @@ export function useTranscriptScroll({
|
|
|
298
375
|
} else {
|
|
299
376
|
const lr = store.getLineRectAt?.(y);
|
|
300
377
|
if (lr) { mLo = { x: lr.x1, y: lr.y1 }; mHi = { x: lr.x2, y: lr.y2 }; }
|
|
301
|
-
else { mLo = { x: 0, y }; mHi = { x, y }; }
|
|
378
|
+
else { mLo = { x: 0, y }; mHi = { x: Math.max(0, frameColumns - 1), y }; }
|
|
302
379
|
}
|
|
303
380
|
const rect = (a, b) => ({ mode: 'linear', x1: a.x, y1: a.y, x2: b.x, y2: b.y });
|
|
304
381
|
if (comparePoints(mHi, spanLo) < 0) return rect(spanHi, mLo);
|
|
305
382
|
if (comparePoints(mLo, spanHi) > 0) return rect(spanLo, mHi);
|
|
306
383
|
return rect(spanLo, spanHi);
|
|
307
|
-
}, [store, selectionPointAtCurrentScroll]);
|
|
384
|
+
}, [store, frameColumns, selectionPointAtCurrentScroll]);
|
|
308
385
|
|
|
309
386
|
const transcriptViewportRows = useCallback(() => {
|
|
310
387
|
const top = Math.max(0, Number(transcriptViewportRef.current?.top) || 0);
|
|
@@ -356,6 +433,17 @@ export function useTranscriptScroll({
|
|
|
356
433
|
const maxTarget = Math.max(0, Number(maxScrollRowsRef.current) || 0);
|
|
357
434
|
const target = Math.max(0, Math.min(maxTarget, scrollTargetRef.current + deltaRows));
|
|
358
435
|
const appliedDelta = target - scrollTargetRef.current;
|
|
436
|
+
// Before the scroll moves selected rows out of view, snapshot the rows
|
|
437
|
+
// currently under the selection into the stitch buffer keyed by the
|
|
438
|
+
// PRE-scroll offset (ref selection.ts captureScrolledRows). Runs for BOTH
|
|
439
|
+
// an active drag and a wheel-shift of a released selection, so Ctrl+C
|
|
440
|
+
// reconstructs the full text no matter how far it scrolled off-screen.
|
|
441
|
+
if (appliedDelta !== 0 && dragRef.current.region === 'transcript' && dragRef.current.rect) {
|
|
442
|
+
// Commit any pending throttled rect first so the harvest reads the newest
|
|
443
|
+
// rendered selection (not the previous rect) before those rows scroll off.
|
|
444
|
+
flushPendingSelectionPaint();
|
|
445
|
+
harvestStitchRowsNow(Number(scrollTargetRef.current) || 0);
|
|
446
|
+
}
|
|
359
447
|
// Any manual wheel/keyboard scroll takes precedence over an in-flight
|
|
360
448
|
// transcript follow: drop the glide so the user's intent wins.
|
|
361
449
|
if (appliedDelta !== 0) cancelTranscriptFollow();
|
|
@@ -411,8 +499,17 @@ export function useTranscriptScroll({
|
|
|
411
499
|
} else {
|
|
412
500
|
rect = shiftSelectionRectY(dragRef.current.rect, appliedDelta);
|
|
413
501
|
}
|
|
414
|
-
|
|
502
|
+
// Active-drag rebuild paints directly, so route through the themed clip
|
|
503
|
+
// (captureText:false, matching rememberText:false below) — a bare rect
|
|
504
|
+
// without selectionBackground falls back to a near-white full-width block
|
|
505
|
+
// with vanishing text (vendor/ink output.js). Also cancel any armed
|
|
506
|
+
// throttled repaint first: it would fire the pre-scroll rect AFTER this
|
|
507
|
+
// one, leaving two coexisting highlights.
|
|
508
|
+
const clippedRect = dragRef.current.active
|
|
509
|
+
? withSelectionClip(rect, { captureText: false })
|
|
510
|
+
: withSelectionClip(rect);
|
|
415
511
|
dragRef.current = { ...dragRef.current, rect: clippedRect };
|
|
512
|
+
cancelPendingSelectionPaint();
|
|
416
513
|
// Never re-harvest selection text from a scroll-shifted rect: the shift
|
|
417
514
|
// clips the rect to the viewport, so a harvest here would OVERWRITE the
|
|
418
515
|
// full text remembered at drag-release with only the still-visible rows
|
|
@@ -426,7 +523,7 @@ export function useTranscriptScroll({
|
|
|
426
523
|
stopSmoothScroll();
|
|
427
524
|
scrollPositionRef.current = target;
|
|
428
525
|
setScrollOffset(Math.round(target));
|
|
429
|
-
}, [startSmoothScroll, stopSmoothScroll, paintSelectionRect, selectionPointAtCurrentScroll, withSelectionClip, cancelTranscriptFollow, buildSpanRect]);
|
|
526
|
+
}, [startSmoothScroll, stopSmoothScroll, paintSelectionRect, selectionPointAtCurrentScroll, withSelectionClip, cancelTranscriptFollow, buildSpanRect, harvestStitchRowsNow, cancelPendingSelectionPaint, flushPendingSelectionPaint]);
|
|
430
527
|
|
|
431
528
|
// Leading-edge coalescer for edge-drag auto-scroll + wheel deltas: the first
|
|
432
529
|
// delta after an idle period flushes immediately (single wheel ticks/short
|
|
@@ -60,6 +60,18 @@ export function useTranscriptWindow({
|
|
|
60
60
|
}) {
|
|
61
61
|
const transcriptTotalRowsRef = useRef(0);
|
|
62
62
|
const preservedScrollDeltaRef = useRef(0);
|
|
63
|
+
// Pessimistic "committed" max-scroll for the IMMEDIATE wheel/keyboard clamp.
|
|
64
|
+
// transcriptWindow.maxScrollRows is derived from the row index, which uses
|
|
65
|
+
// ESTIMATED heights for rows in the mounted slice. On the frame a scroll-up
|
|
66
|
+
// FIRST mounts a new row, its estimate may overshoot the real Yoga height, so
|
|
67
|
+
// that frame's maxScrollRows is inflated and a wheel offset clamped only
|
|
68
|
+
// against it scrolls past committed geometry — opening a one-frame blank band
|
|
69
|
+
// at the top until the post-commit harvest corrects it. We hold this cap only
|
|
70
|
+
// while a row IN the mounted slice is still unmeasured; the harvest measures
|
|
71
|
+
// it on the next commit and the estimate is adopted. Expansion can never be
|
|
72
|
+
// blocked longer than that frame (and is bypassed entirely when measured-rows
|
|
73
|
+
// mode is off — no harvest ever runs, so the estimate is the only geometry).
|
|
74
|
+
const committedMaxScrollRowsRef = useRef(0);
|
|
63
75
|
// Per-hook-instance settled-prefix row-index cache for the incremental
|
|
64
76
|
// builder. Was module-level (leaked across hook instances); now local so
|
|
65
77
|
// each transcript window owns its own tail-flush cache.
|
|
@@ -389,7 +401,49 @@ export function useTranscriptWindow({
|
|
|
389
401
|
rowIndex: transcriptRowIndex,
|
|
390
402
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- intentional: sig+scroll/viewport capture the relevant changes
|
|
391
403
|
}), [transcriptStructureSig, renderScrollOffset, transcriptContentHeight, transcriptRowIndex]);
|
|
392
|
-
|
|
404
|
+
// Publish the max for the immediate wheel/keyboard clamp (see the
|
|
405
|
+
// committedMaxScrollRowsRef note above). Adopt the estimate-based max unless a
|
|
406
|
+
// row in THIS frame's mounted slice is still unmeasured — that is the only
|
|
407
|
+
// case where an over-estimated freshly-mounted row can inflate maxScrollRows
|
|
408
|
+
// ahead of committed Yoga geometry. Off-slice appended rows (bottom spacer,
|
|
409
|
+
// never mounted) don't trigger the hold, so scroll can always reach the true
|
|
410
|
+
// oldest rows; and with measured-rows mode off, no harvest ever runs, so the
|
|
411
|
+
// raw estimate is published unconditionally.
|
|
412
|
+
const estimateMaxScrollRows = Math.max(0, Number(transcriptWindow.maxScrollRows) || 0);
|
|
413
|
+
let holdCommittedMax = false;
|
|
414
|
+
if (TRANSCRIPT_MEASURED_ROWS && estimateMaxScrollRows > committedMaxScrollRowsRef.current) {
|
|
415
|
+
// Expansion requested: only trust it once every mounted-slice row has a
|
|
416
|
+
// committed measurement that is VALID FOR THIS geometry. A just-mounted row
|
|
417
|
+
// lacks a cache entry until the post-commit harvest records its real height;
|
|
418
|
+
// and a cache entry left over from a prior columns/tool-expanded/variant is
|
|
419
|
+
// stale (the harvest re-measures it against these same fields), so it must
|
|
420
|
+
// also count as unmeasured — otherwise the pre-harvest estimate for the new
|
|
421
|
+
// geometry could re-open the overshoot right after such a change.
|
|
422
|
+
const toolExpandedFlag = toolOutputExpanded ? 1 : 0;
|
|
423
|
+
const mountedSlice = transcriptWindow.items || [];
|
|
424
|
+
for (let i = 0; i < mountedSlice.length; i++) {
|
|
425
|
+
const it = mountedSlice[i];
|
|
426
|
+
if (!it || shouldSuppressFullyFailedToolItem(it)) continue;
|
|
427
|
+
if (it.kind === 'assistant' && it.streaming) {
|
|
428
|
+
const idPrev = streamingMeasuredRowsById.get(it.id);
|
|
429
|
+
if (!idPrev || idPrev.columns !== frameColumns || idPrev.toolExpanded !== toolExpandedFlag) {
|
|
430
|
+
holdCommittedMax = true;
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
} else {
|
|
434
|
+
const prev = transcriptMeasuredRowsCache.get(it);
|
|
435
|
+
if (!prev
|
|
436
|
+
|| prev.columns !== frameColumns
|
|
437
|
+
|| prev.toolExpanded !== toolExpandedFlag
|
|
438
|
+
|| prev.variantKey !== transcriptItemVariantKey(it)) {
|
|
439
|
+
holdCommittedMax = true;
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (!holdCommittedMax) committedMaxScrollRowsRef.current = estimateMaxScrollRows;
|
|
446
|
+
maxScrollRowsRef.current = committedMaxScrollRowsRef.current;
|
|
393
447
|
// Publish this frame's geometry so a manual scroll can capture the reading
|
|
394
448
|
// anchor synchronously (see captureTranscriptAnchorAt).
|
|
395
449
|
transcriptGeomRef.current = {
|
|
@@ -71,7 +71,7 @@ export const UserMessage = React.memo(function UserMessage({ text, attached = fa
|
|
|
71
71
|
const bandColumns = Math.max(1, columns - 1);
|
|
72
72
|
return (
|
|
73
73
|
<Box flexDirection="column" width={bandColumns} marginTop={attached ? 0 : 1} backgroundColor={theme.userMessageBackground} paddingLeft={2} paddingRight={1}>
|
|
74
|
-
<Text color={theme.
|
|
74
|
+
<Text color={theme.mixdogIvory} wrap="wrap">{text}</Text>
|
|
75
75
|
</Box>
|
|
76
76
|
);
|
|
77
77
|
});
|
|
@@ -660,7 +660,9 @@ export function PromptInput({
|
|
|
660
660
|
}
|
|
661
661
|
|
|
662
662
|
if (!commandPaletteActive && ((key.ctrl && inputKey === 'v') || (key.meta && inputKey === 'v'))) {
|
|
663
|
-
|
|
663
|
+
// Ctrl+V / Meta+V: read OS clipboard (text first, image fallback) — the
|
|
664
|
+
// empty text arg tags the shortcut path in handlePromptPaste.
|
|
665
|
+
handleExternalPaste('', { source: 'clipboard-shortcut' });
|
|
664
666
|
return;
|
|
665
667
|
}
|
|
666
668
|
|
|
@@ -7,13 +7,20 @@
|
|
|
7
7
|
* line is promoted to a real transcript user row only when it starts executing
|
|
8
8
|
* (see engine drain()).
|
|
9
9
|
*
|
|
10
|
+
* Default (expanded) mode renders the FULL wrapped text at the same content
|
|
11
|
+
* width the promoted transcript user row uses, so promotion does not change
|
|
12
|
+
* the row height mid-flight (the old 1-line truncation made the frame visibly
|
|
13
|
+
* jump when a wrapped message expanded on promotion). App.jsx reserves the
|
|
14
|
+
* matching height via queuedBandRows() and flips `compact` on when the queue
|
|
15
|
+
* would not fit the frame — compact mode truncates each entry to one row.
|
|
16
|
+
*
|
|
10
17
|
* Renders nothing when the queue is empty.
|
|
11
18
|
*/
|
|
12
19
|
import React from 'react';
|
|
13
20
|
import { Box, Text } from 'ink';
|
|
14
21
|
import { theme } from '../theme.mjs';
|
|
15
22
|
|
|
16
|
-
export function QueuedCommands({ queued, columns }) {
|
|
23
|
+
export function QueuedCommands({ queued, columns, compact = false }) {
|
|
17
24
|
if (!queued || queued.length === 0) return null;
|
|
18
25
|
// Each queued line reads as a full-width band like a user message (same
|
|
19
26
|
// background, 2-col gutter), but stays readable while it waits to be sent.
|
|
@@ -21,19 +28,23 @@ export function QueuedCommands({ queued, columns }) {
|
|
|
21
28
|
// One cell short of the edge: writing the last terminal column triggers
|
|
22
29
|
// Windows auto-wrap/scroll that drifts the alt-screen frame (see UserMessage).
|
|
23
30
|
const bandColumns = Math.max(1, columns - 1);
|
|
31
|
+
// Content width = bandColumns(columns-1) - paddingLeft(2) - paddingRight(1)
|
|
32
|
+
// = columns-4. Compact truncation appends '…' (1 cell), so the slice must
|
|
33
|
+
// leave room for that suffix and avoid a wrap to row 2.
|
|
34
|
+
const contentWidth = Math.max(1, columns - 4);
|
|
24
35
|
return (
|
|
25
36
|
<Box flexDirection="column">
|
|
26
37
|
{queued.map((item) => {
|
|
27
|
-
// Truncate to 1 line so the row reservation (queued.length in App.jsx)
|
|
28
|
-
// stays accurate — wrapped text would push the input box off-screen.
|
|
29
|
-
// Content width = bandColumns(columns-1) - paddingLeft(2) - paddingRight(1)
|
|
30
|
-
// = columns-4. When truncating we append '…' (1 cell), so the slice
|
|
31
|
-
// must leave room for that suffix and avoid a wrap to row 2.
|
|
32
|
-
const contentWidth = Math.max(1, columns - 4);
|
|
33
38
|
const sourceText = String(item.displayText || item.text || '');
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
:
|
|
39
|
+
let displayText = sourceText;
|
|
40
|
+
if (compact) {
|
|
41
|
+
// Compact fallback: exactly 1 row per entry (queued.length reserve).
|
|
42
|
+
// Collapse newlines first — a raw '\n' would still break the row.
|
|
43
|
+
const oneLine = sourceText.replace(/\r?\n/g, ' ');
|
|
44
|
+
displayText = oneLine.length > contentWidth
|
|
45
|
+
? (contentWidth <= 1 ? '…'.repeat(contentWidth) : oneLine.slice(0, Math.max(1, contentWidth - 1)) + '…')
|
|
46
|
+
: oneLine;
|
|
47
|
+
}
|
|
37
48
|
return (
|
|
38
49
|
<Box key={item.id} width={bandColumns} backgroundColor={theme.userMessageBackground} paddingLeft={2} paddingRight={1}>
|
|
39
50
|
<Text wrap="wrap">
|
|
@@ -295,8 +295,8 @@ function localOldestWorkerStartMs(agentWorkers = [], agentJobs = []) {
|
|
|
295
295
|
// L2 assembly only — themed SGR (statusColors SUCCESS/STATUS/SUBTLE), already in
|
|
296
296
|
// the active palette, so this must NOT be passed through normalizeStatusLine.
|
|
297
297
|
// Returns the joined L2 string or '' when no active segment. Order:
|
|
298
|
-
// Agents → Exploring → Searching → Shells. (Shell segment is disk-based and
|
|
299
|
-
// available to the instant-local path; intentionally omitted here.)
|
|
298
|
+
// Agents → Exploring → Web Searching → Shells. (Shell segment is disk-based and
|
|
299
|
+
// not available to the instant-local path; intentionally omitted here.)
|
|
300
300
|
function localStatusLineL2({
|
|
301
301
|
agentWorkers = [],
|
|
302
302
|
agentJobs = [],
|
|
@@ -325,7 +325,7 @@ function localStatusLineL2({
|
|
|
325
325
|
}
|
|
326
326
|
if (searchInfo && localNum(searchInfo.count) > 0) {
|
|
327
327
|
const elapsed = localNum(searchInfo.startedAt) > 0 ? localFormatElapsed(now - localNum(searchInfo.startedAt)) : '';
|
|
328
|
-
l2Parts.push(`${spin} ${STATUS}Searching${RESET}${elapsedSuffix(elapsed)}`);
|
|
328
|
+
l2Parts.push(`${spin} ${STATUS}Web Searching${RESET}${elapsedSuffix(elapsed)}`);
|
|
329
329
|
}
|
|
330
330
|
return l2Parts.length ? l2Parts.join(segSep) : '';
|
|
331
331
|
}
|
|
@@ -83,7 +83,7 @@ function statusCopy(name, label, count, doneCount, pending, isError, args = {})
|
|
|
83
83
|
// dropping the pad just normalizes the spacing.
|
|
84
84
|
return formatToolActionHeader(name, args, { pending, count });
|
|
85
85
|
}
|
|
86
|
-
export function ToolExecution({ name, args, result, rawResult, isError, errorCount, expanded, columns = 80, attached = false, count = 1, completedCount = 0, startedAt = 0, completedAt = 0, aggregate = false, categories = {}, headerFinalized = true, deferredDisplayReady = false }) {
|
|
86
|
+
export function ToolExecution({ name, args, result, rawResult, isError, errorCount, expanded, columns = 80, attached = false, count = 1, completedCount = 0, startedAt = 0, completedAt = 0, aggregate = false, categories = {}, doneCategories = null, headerFinalized = true, deferredDisplayReady = false }) {
|
|
87
87
|
const rowWidth = Math.max(1, Number(columns || 80));
|
|
88
88
|
const [blinkOn, setBlinkOn] = useState(true);
|
|
89
89
|
const [blinkExpired, setBlinkExpired] = useState(false);
|
|
@@ -120,6 +120,18 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
120
120
|
const failedCount = clampFailureCount(errorCount, groupCount, isError);
|
|
121
121
|
const displayGroupCount = groupCount;
|
|
122
122
|
const displayCategories = normalizeCountMap(categories || {});
|
|
123
|
+
// In the DONE state, count only successful calls: error-terminated calls are
|
|
124
|
+
// excluded from the category totals via the engine-supplied doneCategories
|
|
125
|
+
// map (failures still surface in the 'N Ok · N Failed' detail). The pending/
|
|
126
|
+
// in-flight header keeps using the raw call-time counts unchanged.
|
|
127
|
+
const normalizedDoneCategories = doneCategories ? normalizeCountMap(doneCategories) : displayCategories;
|
|
128
|
+
// All-failed aggregate collapses doneCategories to zero counts, which would
|
|
129
|
+
// render a blank header. Fall back to the raw call-time counts so the done
|
|
130
|
+
// header is never empty; the 'N Failed' detail still marks the failure.
|
|
131
|
+
const hasDoneCounts = Object.values(normalizedDoneCategories || {}).some(
|
|
132
|
+
(v) => (v && typeof v === 'object' ? Number(v.count || 0) : Number(v || 0)) > 0,
|
|
133
|
+
);
|
|
134
|
+
const displayDoneCategories = hasDoneCounts ? normalizedDoneCategories : displayCategories;
|
|
123
135
|
|
|
124
136
|
useEffect(() => {
|
|
125
137
|
if (!pending) {
|
|
@@ -206,7 +218,7 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
206
218
|
// No stableVerbWidth: see statusCopy — the padding only left a mid-header
|
|
207
219
|
// gap ("Searched 1 pattern, Read 1 file") since Ink trims trailing
|
|
208
220
|
// spaces and never stabilized the flip.
|
|
209
|
-
const headerText = safeInlineText(formatAggregateHeader(displayCategories || {}, { pending: headerPending, order: headerOrder }));
|
|
221
|
+
const headerText = safeInlineText(formatAggregateHeader((headerPending ? displayCategories : displayDoneCategories) || {}, { pending: headerPending, order: headerOrder }));
|
|
210
222
|
let detailText;
|
|
211
223
|
if (hasResult) {
|
|
212
224
|
// The aggregate card reserves EXACTLY ONE detail row when it is not
|
|
@@ -456,7 +468,7 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
456
468
|
labelText = safeInlineText(labelText);
|
|
457
469
|
// Show the parenthesized arg summary for grouped cards too, matching single
|
|
458
470
|
// calls so the header carries the same context.
|
|
459
|
-
const toolSearchSummary = !pending && normalizedName === '
|
|
471
|
+
const toolSearchSummary = !pending && normalizedName === 'load_tool' && hasResult
|
|
460
472
|
? toolSearchLoadedSummary(displayedResultText)
|
|
461
473
|
: '';
|
|
462
474
|
const rawSummaryText = safeInlineText(isAgentResponse || isBackgroundResponse
|
|
@@ -486,7 +498,7 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
486
498
|
&& hasDisplayBody
|
|
487
499
|
&& (totalLines > 1 || firstResultLineClipped || Boolean(shellCollapsedSummary && shellCollapsedSummary !== firstResultLine));
|
|
488
500
|
const showHeaderExpandHint = (isShellSurface ? shellHasExpandableBody : (isAgentSurfaceCard ? agentHasExpandableBody : (hasHiddenDetail || backgroundMetadataExpandable)))
|
|
489
|
-
&& normalizedName !== '
|
|
501
|
+
&& normalizedName !== 'load_tool';
|
|
490
502
|
const expandHintColor = theme.subtle;
|
|
491
503
|
|
|
492
504
|
// Build a single-line header that never wraps: reserve width for the fixed
|
|
@@ -77,7 +77,7 @@ export const Item = React.memo(function Item({ item, prevKind, columns, toolOutp
|
|
|
77
77
|
node = <ToolHookDenialCard item={item} columns={columns} />;
|
|
78
78
|
break;
|
|
79
79
|
}
|
|
80
|
-
node = <ToolExecution name={item.name} args={item.args} result={item.result} rawResult={item.rawResult} isError={item.isError} errorCount={item.errorCount} expanded={toolOutputExpanded || item.expanded} columns={columns} attached={false} count={item.count} completedCount={item.completedCount} startedAt={item.startedAt} completedAt={item.completedAt} aggregate={item.aggregate} categories={item.categories} headerFinalized={item.headerFinalized} deferredDisplayReady={item.deferredDisplayReady} themeEpoch={themeEpoch} />;
|
|
80
|
+
node = <ToolExecution name={item.name} args={item.args} result={item.result} rawResult={item.rawResult} isError={item.isError} errorCount={item.errorCount} expanded={toolOutputExpanded || item.expanded} columns={columns} attached={false} count={item.count} completedCount={item.completedCount} startedAt={item.startedAt} completedAt={item.completedAt} aggregate={item.aggregate} categories={item.categories} doneCategories={item.doneCategories} headerFinalized={item.headerFinalized} deferredDisplayReady={item.deferredDisplayReady} themeEpoch={themeEpoch} />;
|
|
81
81
|
break;
|
|
82
82
|
}
|
|
83
83
|
case 'notice':
|