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,10 +84,10 @@ export function createSlashDispatch({
|
|
|
84
84
|
return true;
|
|
85
85
|
}
|
|
86
86
|
case 'search':
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
// No busy guard: /search only picks the search provider/model (a config
|
|
88
|
+
// save consumed by the NEXT search tool call). It never touches the
|
|
89
|
+
// in-flight turn, and the same picker is already reachable mid-turn via
|
|
90
|
+
// /settings, so blocking it here was inconsistent.
|
|
91
91
|
if (arg) store.pushNotice('/search sets the search provider/model; the search tool uses that model when called.', 'warn');
|
|
92
92
|
openSearchPicker();
|
|
93
93
|
return true;
|
|
@@ -57,6 +57,17 @@ export function wrappedDetailRows(text, width) {
|
|
|
57
57
|
return wrapAnsi(value, w, { trim: false, hard: true }).split('\n').length;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// Rows one queued steering band (QueuedCommands expanded mode) occupies when
|
|
61
|
+
// its full text renders with ink's wrap="wrap" at `width` content columns.
|
|
62
|
+
// Same wrap-ansi call ink uses, so the queuedRows reservation in App.jsx stays
|
|
63
|
+
// in lock-step with the rendered height — a mismatch pushes the input box.
|
|
64
|
+
export function queuedBandRows(text, width) {
|
|
65
|
+
const value = String(text ?? '');
|
|
66
|
+
if (!value) return 1;
|
|
67
|
+
const w = Math.max(1, Math.floor(Number(width) || 1));
|
|
68
|
+
return Math.max(1, wrapAnsi(value, w, { trim: false, hard: true }).split('\n').length);
|
|
69
|
+
}
|
|
70
|
+
|
|
60
71
|
// Character offsets where each visual row starts (hard wrap + explicit newlines).
|
|
61
72
|
export function visualRowStartOffsets(text, width) {
|
|
62
73
|
const value = String(text ?? '');
|
|
@@ -28,6 +28,14 @@ const MOUSE_CTRL_MASK = 16;
|
|
|
28
28
|
// every modifier bit) so a shift-held left-click can extend an existing
|
|
29
29
|
// selection instead of starting a fresh one.
|
|
30
30
|
const MOUSE_SHIFT_MASK = 4;
|
|
31
|
+
// Windows Terminal (1.25+) forwards shift+click/drag to the app during VT
|
|
32
|
+
// mouse mode while ALSO painting its own native selection — honoring the
|
|
33
|
+
// events would draw two overlapping highlights (app blue + WT white). In WT,
|
|
34
|
+
// drop every shift-modified button event so shift stays purely native
|
|
35
|
+
// (native highlight + native Ctrl+C copy); plain drag / ctrl+click /
|
|
36
|
+
// right-click remain the app-owned selection paths. Other terminals keep
|
|
37
|
+
// the shift-extend behavior (with XTSHIFTESCAPE opted in from index.jsx).
|
|
38
|
+
const IS_WINDOWS_TERMINAL = Boolean(process.env.WT_SESSION);
|
|
31
39
|
|
|
32
40
|
export function useMouseInput({
|
|
33
41
|
inkInput,
|
|
@@ -56,7 +64,10 @@ export function useMouseInput({
|
|
|
56
64
|
setMeasuredRowsVersion,
|
|
57
65
|
clearStitchBuffer,
|
|
58
66
|
}) {
|
|
59
|
-
const
|
|
67
|
+
const zoomTimerRef = useRef(null);
|
|
68
|
+
// Edge auto-scroll timer state (see the effect below). Owned at hook scope so
|
|
69
|
+
// it survives re-subscribes; the drag itself lives on the App-owned dragRef.
|
|
70
|
+
const edgeAutoscrollRef = useRef({ dir: 0, timer: null, noMove: 0 });
|
|
60
71
|
|
|
61
72
|
const passthroughCtrlWheelZoom = useCallback(() => {
|
|
62
73
|
if (!stdout?.write) return;
|
|
@@ -65,21 +76,21 @@ export function useMouseInput({
|
|
|
65
76
|
} catch {
|
|
66
77
|
return;
|
|
67
78
|
}
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
79
|
+
if (zoomTimerRef.current) clearTimeout(zoomTimerRef.current);
|
|
80
|
+
zoomTimerRef.current = setTimeout(() => {
|
|
81
|
+
zoomTimerRef.current = null;
|
|
71
82
|
try {
|
|
72
83
|
stdout.write(ALT_SCROLL_ON + MOUSE_TRACKING_ON);
|
|
73
84
|
} catch {
|
|
74
85
|
// The terminal may already be closing.
|
|
75
86
|
}
|
|
76
87
|
}, 700);
|
|
77
|
-
|
|
78
|
-
}, [stdout]);
|
|
88
|
+
zoomTimerRef.current.unref?.();
|
|
89
|
+
}, [stdout, zoomTimerRef]);
|
|
79
90
|
|
|
80
91
|
useEffect(() => () => {
|
|
81
|
-
if (
|
|
82
|
-
}, []);
|
|
92
|
+
if (zoomTimerRef.current) clearTimeout(zoomTimerRef.current);
|
|
93
|
+
}, [zoomTimerRef]);
|
|
83
94
|
|
|
84
95
|
// Optional mouse handling. When index.jsx enables SGR mouse tracking
|
|
85
96
|
// (?1000h button + ?1002h drag-motion + ?1006h SGR coords).
|
|
@@ -175,6 +186,83 @@ export function useMouseInput({
|
|
|
175
186
|
promptMouseSelectionRef.current?.clear?.();
|
|
176
187
|
applySelectionRect(null);
|
|
177
188
|
};
|
|
189
|
+
// Edge auto-scroll TIMER (ref: ScrollKeybindingHandler useDragToScroll).
|
|
190
|
+
// SGR mode 1002 reports drag-motion only when the pointer changes CELL, so
|
|
191
|
+
// a pointer held stationary at the top/bottom edge stops emitting events
|
|
192
|
+
// and the motion-driven scroll below stalls. This interval keeps scrolling
|
|
193
|
+
// — scrollTranscriptRows' active-drag branch re-extends the selection to
|
|
194
|
+
// the still-held `last` cell each step — until the pointer leaves the edge,
|
|
195
|
+
// the drag ends, or a scroll boundary is reached (delta clamps to 0).
|
|
196
|
+
const EDGE_AUTOSCROLL_INTERVAL_MS = 50;
|
|
197
|
+
const stopEdgeAutoscroll = () => {
|
|
198
|
+
const st = edgeAutoscrollRef.current;
|
|
199
|
+
if (st.timer) { clearInterval(st.timer); st.timer = null; }
|
|
200
|
+
st.dir = 0;
|
|
201
|
+
st.noMove = 0;
|
|
202
|
+
};
|
|
203
|
+
const startEdgeAutoscroll = (dir) => {
|
|
204
|
+
const st = edgeAutoscrollRef.current;
|
|
205
|
+
if (st.dir === dir && st.timer) return; // already scrolling this way
|
|
206
|
+
stopEdgeAutoscroll();
|
|
207
|
+
st.dir = dir;
|
|
208
|
+
st.noMove = 0;
|
|
209
|
+
st.timer = setInterval(() => {
|
|
210
|
+
const drag = dragRef.current;
|
|
211
|
+
const st2 = edgeAutoscrollRef.current;
|
|
212
|
+
if (!drag.active || drag.region !== 'transcript' || st2.dir === 0) {
|
|
213
|
+
stopEdgeAutoscroll();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const before = Number(scrollTargetRef.current) || 0;
|
|
217
|
+
queueScrollCoalesced(st2.dir * 3);
|
|
218
|
+
// A SINGLE unchanged tick is ambiguous: the coalescer may not have
|
|
219
|
+
// flushed this delta yet (queued behind its 16ms leading-edge timer),
|
|
220
|
+
// so scrollTarget legitimately reads the same value mid-flight. Only a
|
|
221
|
+
// real top/bottom clamp keeps it pinned across several ticks. Require
|
|
222
|
+
// CONSECUTIVE no-move ticks (>=3 ⇒ >150ms ≫ the 16ms coalescer window,
|
|
223
|
+
// so any pending scroll has certainly flushed) before stopping — any
|
|
224
|
+
// movement resets the counter (ref useDragToScroll's getScrollTop<=0 /
|
|
225
|
+
// >=max boundary stop).
|
|
226
|
+
if ((Number(scrollTargetRef.current) || 0) !== before) {
|
|
227
|
+
st2.noMove = 0;
|
|
228
|
+
} else if (++st2.noMove >= 3) {
|
|
229
|
+
stopEdgeAutoscroll();
|
|
230
|
+
}
|
|
231
|
+
}, EDGE_AUTOSCROLL_INTERVAL_MS);
|
|
232
|
+
st.timer.unref?.();
|
|
233
|
+
};
|
|
234
|
+
// Finalize an in-flight drag exactly like the button-release path: push the
|
|
235
|
+
// final rect from the given point, and reconcile measured row heights. Used
|
|
236
|
+
// by the real release below AND by the ctrl+wheel zoom passthrough, whose
|
|
237
|
+
// mouse-tracking disable can swallow the release event entirely.
|
|
238
|
+
const finalizeActiveDrag = (fx, fy) => {
|
|
239
|
+
const drag = dragRef.current;
|
|
240
|
+
if (!drag.active) return;
|
|
241
|
+
stopEdgeAutoscroll();
|
|
242
|
+
const region = drag.region;
|
|
243
|
+
if (region === 'prompt') {
|
|
244
|
+
const offset = promptOffsetAt(fx, fy);
|
|
245
|
+
drag.active = false;
|
|
246
|
+
promptMouseSelectionRef.current?.extendTo?.(offset, true);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const span = drag.anchorSpan;
|
|
250
|
+
const finalY = region === 'status' ? clampToStatusBand(fy) : clampToTranscriptViewport(fy);
|
|
251
|
+
drag.active = false;
|
|
252
|
+
if (span) {
|
|
253
|
+
const rect = buildSpanRect(span, fx, finalY, region, drag.anchorScroll);
|
|
254
|
+
applySelectionRect(rect);
|
|
255
|
+
} else {
|
|
256
|
+
const anchor = region === 'status'
|
|
257
|
+
? drag.anchor
|
|
258
|
+
: selectionPointAtCurrentScroll(drag.anchor, drag.anchorScroll);
|
|
259
|
+
const rect = linearSelection(anchor, { x: fx, y: finalY });
|
|
260
|
+
const empty = rect.x1 === rect.x2 && rect.y1 === rect.y2;
|
|
261
|
+
if (empty) applySelectionRect(null);
|
|
262
|
+
else applySelectionRect(rect);
|
|
263
|
+
}
|
|
264
|
+
if (TRANSCRIPT_MEASURED_ROWS) setMeasuredRowsVersion((v) => (v + 1) % 1000000);
|
|
265
|
+
};
|
|
178
266
|
// Typed 'mouse' channel handler. Receives one event per emit:
|
|
179
267
|
// • ParsedMouse {kind:'mouse',button,action,col,row,sequence} — click/drag
|
|
180
268
|
// • ParsedKey {kind:'key',name:'wheelup'|'wheeldown',sequence} — wheel
|
|
@@ -193,6 +281,19 @@ export function useMouseInput({
|
|
|
193
281
|
const wm = WHEEL_SGR.exec(seq);
|
|
194
282
|
const ctrl = wm ? ((Number(wm[1]) & MOUSE_CTRL_MASK) !== 0) : false;
|
|
195
283
|
if (ctrl) {
|
|
284
|
+
// Zoom passthrough disables mouse tracking for ~700ms, during which
|
|
285
|
+
// the button-release event may never arrive — leaving the drag stuck
|
|
286
|
+
// active and the edge-autoscroll interval firing forever. Finalize
|
|
287
|
+
// the in-flight drag from its last known point (same path as a real
|
|
288
|
+
// release: final applySelectionRect + measured-rows reconcile) so
|
|
289
|
+
// Ctrl+C still copies the full text, then hand the wheel to the
|
|
290
|
+
// terminal's font zoom. stopEdgeAutoscroll is folded into finalize.
|
|
291
|
+
if (dragRef.current.active) {
|
|
292
|
+
const last = dragRef.current.last || {};
|
|
293
|
+
finalizeActiveDrag(Number(last.x) || 0, Number(last.y) || 0);
|
|
294
|
+
} else {
|
|
295
|
+
stopEdgeAutoscroll();
|
|
296
|
+
}
|
|
196
297
|
passthroughCtrlWheelZoom();
|
|
197
298
|
return;
|
|
198
299
|
}
|
|
@@ -201,9 +302,8 @@ export function useMouseInput({
|
|
|
201
302
|
// Fall through to the wheel-scroll dispatch below (shared with the
|
|
202
303
|
// previous SGR path) so slash-palette/overlay/scroll routing is identical.
|
|
203
304
|
if (up !== 0 || down !== 0) {
|
|
204
|
-
if (dragRef.current.active) return;
|
|
205
305
|
const palette = slashPaletteRef.current;
|
|
206
|
-
if (palette.open && palette.count > 0) {
|
|
306
|
+
if (!dragRef.current.active && palette.open && palette.count > 0) {
|
|
207
307
|
const step = down - up;
|
|
208
308
|
if (step !== 0) {
|
|
209
309
|
setSlashIndex((index) => Math.max(0, Math.min(palette.count - 1, index + step)));
|
|
@@ -212,6 +312,11 @@ export function useMouseInput({
|
|
|
212
312
|
}
|
|
213
313
|
if (overlayBlocksGlobalTranscriptScroll(scrollFocusRef.current)) return;
|
|
214
314
|
const STEP = 3; // rows per wheel notch; immediate updates feel steadier in Windows Terminal
|
|
315
|
+
// Wheel while a selection is live (mid-drag OR after release) scrolls
|
|
316
|
+
// the transcript instead of being dropped: scrollTranscriptRows'
|
|
317
|
+
// active-drag branch rebuilds the rect (anchor→last), the released
|
|
318
|
+
// branch shifts it — both keep the highlight and stitch-harvest the
|
|
319
|
+
// rows that scroll off (ref ScrollKeybindingHandler wheel path).
|
|
215
320
|
queueScrollCoalesced((up - down) * STEP);
|
|
216
321
|
}
|
|
217
322
|
return;
|
|
@@ -229,7 +334,86 @@ export function useMouseInput({
|
|
|
229
334
|
// (button & 3); MOUSE_MODIFIER_MASK above is scroll-routing-only and
|
|
230
335
|
// deliberately treats shift as noise there.
|
|
231
336
|
const shiftHeld = (button & MOUSE_SHIFT_MASK) !== 0;
|
|
337
|
+
const ctrlHeld = (button & MOUSE_CTRL_MASK) !== 0;
|
|
338
|
+
// WT reserves shift+mouse for its NATIVE selection and (per its docs)
|
|
339
|
+
// never forwards those events; keep this guard as a belt-and-braces so
|
|
340
|
+
// a future WT that starts forwarding them can never double-paint.
|
|
341
|
+
if (IS_WINDOWS_TERMINAL && shiftHeld) return;
|
|
342
|
+
// WT's native (shift+drag) selection overlay is NOT cleared by our
|
|
343
|
+
// incremental repaints — it would sit on top of the app selection as a
|
|
344
|
+
// second highlight. Any app-owned button press means the user is done
|
|
345
|
+
// with that native selection: force one full clear+rewrite frame
|
|
346
|
+
// (BSU/ESU-wrapped, no flash) which dismisses it before we paint ours.
|
|
347
|
+
if (IS_WINDOWS_TERMINAL && press && !isMotion) {
|
|
348
|
+
store.forceRenderRepaint?.();
|
|
349
|
+
}
|
|
350
|
+
// Ctrl+left-click is the app-side extend trigger (baseButton =
|
|
351
|
+
// button & 3 already maps ctrl+left press to button 0); right-button
|
|
352
|
+
// press is the second trigger (its own block below). This is
|
|
353
|
+
// unrelated to the ctrl+WHEEL zoom passthrough, which is handled in
|
|
354
|
+
// the wheel path and never reaches this button-press code.
|
|
355
|
+
const extendHeld = shiftHeld || ctrlHeld;
|
|
356
|
+
const isRightPress = baseButton === 2 && press && !isMotion;
|
|
357
|
+
if (isRightPress) {
|
|
358
|
+
// Right-button press = extend-click. Extends the existing selection in
|
|
359
|
+
// the pressed region using the SAME logic as the shift/ctrl paths, but
|
|
360
|
+
// ONLY when there is something extendable there; with nothing to extend
|
|
361
|
+
// it is ignored entirely (no selection clear, no drag start).
|
|
362
|
+
if (isInPromptBox(x, y)) {
|
|
363
|
+
const ctl = promptMouseSelectionRef.current;
|
|
364
|
+
if (ctl?.hasSelection?.()) {
|
|
365
|
+
applySelectionRect(null);
|
|
366
|
+
const offset = promptOffsetAt(x, y);
|
|
367
|
+
stopSmoothScroll();
|
|
368
|
+
// Right-click extend is a one-shot: apply immediately and leave the
|
|
369
|
+
// drag INACTIVE so the right-button release can't finalize (the
|
|
370
|
+
// generic !press branch is left-button-only anyway).
|
|
371
|
+
dragRef.current = { anchor: { x, y }, anchorScroll: 0, last: { x, y }, active: false, rect: null, region: 'prompt', anchorSpan: null };
|
|
372
|
+
if (offset != null) ctl.extendTo?.(offset, true);
|
|
373
|
+
lastClickRef.current = { x: -1, y: -1, t: 0 };
|
|
374
|
+
}
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const inTranscriptR = isInTranscriptViewport(y);
|
|
378
|
+
const inStatusR = !inTranscriptR && isInStatusBand(y);
|
|
379
|
+
if (!inTranscriptR && !inStatusR) return;
|
|
380
|
+
const regionR = inTranscriptR ? 'transcript' : 'status';
|
|
381
|
+
const nowR = Date.now();
|
|
382
|
+
if (dragRef.current.region === regionR && dragRef.current.anchorSpan) {
|
|
383
|
+
promptMouseSelectionRef.current?.clear?.();
|
|
384
|
+
const selectionY = regionR === 'status' ? clampToStatusBand(y) : clampToTranscriptViewport(y);
|
|
385
|
+
const span = dragRef.current.anchorSpan;
|
|
386
|
+
const rect = buildSpanRect(span, x, selectionY, regionR, dragRef.current.anchorScroll);
|
|
387
|
+
stopSmoothScroll();
|
|
388
|
+
dragRef.current = { ...dragRef.current, last: { x, y: selectionY }, active: false, region: regionR };
|
|
389
|
+
applySelectionRect(rect);
|
|
390
|
+
lastClickRef.current = { x, y, t: nowR, count: 1 };
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (
|
|
394
|
+
dragRef.current.region === regionR
|
|
395
|
+
&& !dragRef.current.anchorSpan
|
|
396
|
+
&& dragRef.current.anchor
|
|
397
|
+
&& dragRef.current.rect
|
|
398
|
+
&& !(dragRef.current.rect.x1 === dragRef.current.rect.x2 && dragRef.current.rect.y1 === dragRef.current.rect.y2)
|
|
399
|
+
) {
|
|
400
|
+
promptMouseSelectionRef.current?.clear?.();
|
|
401
|
+
const selectionY = regionR === 'status' ? clampToStatusBand(y) : clampToTranscriptViewport(y);
|
|
402
|
+
const anchor = regionR === 'status'
|
|
403
|
+
? dragRef.current.anchor
|
|
404
|
+
: selectionPointAtCurrentScroll(dragRef.current.anchor, dragRef.current.anchorScroll);
|
|
405
|
+
const rect = linearSelection(anchor, { x, y: selectionY });
|
|
406
|
+
stopSmoothScroll();
|
|
407
|
+
dragRef.current = { ...dragRef.current, last: { x, y: selectionY }, active: false, region: regionR };
|
|
408
|
+
applySelectionRect(rect);
|
|
409
|
+
lastClickRef.current = { x, y, t: nowR, count: 1 };
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
232
414
|
if (baseButton === 0 && press && !isMotion) {
|
|
415
|
+
// A fresh press ends any prior drag's held-at-edge auto-scroll.
|
|
416
|
+
stopEdgeAutoscroll();
|
|
233
417
|
// Region router: a press decides which surface owns this selection.
|
|
234
418
|
// Prompt box takes priority (it overlaps no transcript rows), then the
|
|
235
419
|
// transcript viewport, then the bottom statusline band. A press
|
|
@@ -244,7 +428,7 @@ export function useMouseInput({
|
|
|
244
428
|
// Shift+click extends the EXISTING prompt selection (anchor stays
|
|
245
429
|
// put, cursor jumps to the click) instead of starting a fresh
|
|
246
430
|
// zero-width anchor at the click point.
|
|
247
|
-
if (
|
|
431
|
+
if (extendHeld && ctl?.hasSelection?.()) {
|
|
248
432
|
if (offset != null) ctl.extendTo?.(offset, true);
|
|
249
433
|
lastClickRef.current = { x: -1, y: -1, t: 0 };
|
|
250
434
|
return;
|
|
@@ -295,6 +479,28 @@ export function useMouseInput({
|
|
|
295
479
|
// A press always clears the prompt-box selection (single active region).
|
|
296
480
|
promptMouseSelectionRef.current?.clear?.();
|
|
297
481
|
const now = Date.now();
|
|
482
|
+
// Shift+click on an existing word/line (anchorSpan) selection extends
|
|
483
|
+
// that selection by whole words/lines to the click point, preserving
|
|
484
|
+
// the original anchor span (mirrors selection.ts extendSelection).
|
|
485
|
+
if (
|
|
486
|
+
extendHeld
|
|
487
|
+
&& dragRef.current.region === region
|
|
488
|
+
&& dragRef.current.anchorSpan
|
|
489
|
+
) {
|
|
490
|
+
const selectionY = region === 'status' ? clampToStatusBand(y) : clampToTranscriptViewport(y);
|
|
491
|
+
const span = dragRef.current.anchorSpan;
|
|
492
|
+
const rect = buildSpanRect(span, x, selectionY, region, dragRef.current.anchorScroll);
|
|
493
|
+
stopSmoothScroll();
|
|
494
|
+
dragRef.current = {
|
|
495
|
+
...dragRef.current,
|
|
496
|
+
last: { x, y: selectionY },
|
|
497
|
+
active: true,
|
|
498
|
+
region,
|
|
499
|
+
};
|
|
500
|
+
applySelectionRect(rect);
|
|
501
|
+
lastClickRef.current = { x, y, t: now, count: 1 };
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
298
504
|
// Shift+click extends the existing ink-grid selection in this SAME
|
|
299
505
|
// region from its original anchor to the new click point, instead of
|
|
300
506
|
// starting a fresh anchor here. Only applies to a plain char-drag
|
|
@@ -302,7 +508,7 @@ export function useMouseInput({
|
|
|
302
508
|
// anchorSpan or an empty/absent selection falls through to a normal
|
|
303
509
|
// fresh press below.
|
|
304
510
|
if (
|
|
305
|
-
|
|
511
|
+
extendHeld
|
|
306
512
|
&& dragRef.current.region === region
|
|
307
513
|
&& !dragRef.current.anchorSpan
|
|
308
514
|
&& dragRef.current.anchor
|
|
@@ -432,55 +638,33 @@ export function useMouseInput({
|
|
|
432
638
|
// scroll away the very rows being selected.
|
|
433
639
|
if (y <= 1 && (y < prevDragY || y < top)) {
|
|
434
640
|
queueScrollCoalesced(3);
|
|
641
|
+
startEdgeAutoscroll(1);
|
|
435
642
|
} else if (y >= frameRows - 5 && (y > prevDragY || y > bottom)) {
|
|
436
643
|
queueScrollCoalesced(-3);
|
|
644
|
+
startEdgeAutoscroll(-1);
|
|
645
|
+
} else {
|
|
646
|
+
// Pointer moved back inside the viewport (or along an edge row
|
|
647
|
+
// without pushing toward it): halt any held-at-edge auto-scroll.
|
|
648
|
+
stopEdgeAutoscroll();
|
|
437
649
|
}
|
|
438
650
|
}
|
|
439
|
-
} else if (!press && dragRef.current.active) {
|
|
440
|
-
const region = dragRef.current.region;
|
|
441
|
-
if (region === 'prompt') {
|
|
442
|
-
// Finalize the prompt selection; highlight persists (copy on Ctrl+C).
|
|
443
|
-
const offset = promptOffsetAt(x, y);
|
|
444
|
-
dragRef.current.active = false;
|
|
445
|
-
promptMouseSelectionRef.current?.extendTo?.(offset, true);
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
651
|
+
} else if (!press && baseButton === 0 && dragRef.current.active) {
|
|
448
652
|
// Button release while dragging: finalize with the release coordinate
|
|
449
653
|
// (the SGR release event carries col/row) and keep the selection
|
|
450
654
|
// visible. Copy is NOT automatic — the user presses Ctrl+C to copy.
|
|
451
|
-
// The highlight stays until ESC or a plain click.
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
// word/line at the release cell. A release-without-motion resolves
|
|
458
|
-
// to the span itself (in-span target), so the original word/line
|
|
459
|
-
// highlight stays — never cleared as "empty" like a bare click.
|
|
460
|
-
const rect = buildSpanRect(span, x, releaseY, region, dragRef.current.anchorScroll);
|
|
461
|
-
applySelectionRect(rect);
|
|
462
|
-
} else {
|
|
463
|
-
const anchor = region === 'status'
|
|
464
|
-
? dragRef.current.anchor
|
|
465
|
-
: selectionPointAtCurrentScroll(dragRef.current.anchor, dragRef.current.anchorScroll);
|
|
466
|
-
const rect = linearSelection(anchor, { x, y: releaseY });
|
|
467
|
-
const empty = rect.x1 === rect.x2 && rect.y1 === rect.y2;
|
|
468
|
-
if (empty) {
|
|
469
|
-
applySelectionRect(null); // a plain click clears any prior highlight
|
|
470
|
-
} else {
|
|
471
|
-
// Push the final rect so ink re-renders the visible selection.
|
|
472
|
-
applySelectionRect(rect);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
// Drag is over: the measured-height harvest was skipped for every
|
|
476
|
-
// motion commit (see the harvest effect's dragRef guard), so force one
|
|
477
|
-
// re-measure now to reconcile any row whose true height drifted from
|
|
478
|
-
// its estimate while the drag was in flight.
|
|
479
|
-
if (TRANSCRIPT_MEASURED_ROWS) setMeasuredRowsVersion((v) => (v + 1) % 1000000);
|
|
655
|
+
// The highlight stays until ESC or a plain click. finalizeActiveDrag
|
|
656
|
+
// handles region routing (prompt/word-line/char), the empty→clear
|
|
657
|
+
// case, edge-autoscroll stop, and the measured-rows reconcile — same
|
|
658
|
+
// path the ctrl+wheel zoom passthrough uses. Guarded to baseButton 0
|
|
659
|
+
// so a stray right-button release never finalizes.
|
|
660
|
+
finalizeActiveDrag(x, y);
|
|
480
661
|
}
|
|
481
662
|
}
|
|
482
663
|
};
|
|
483
664
|
inkInput.on('mouse', onMouse);
|
|
484
|
-
return () => {
|
|
665
|
+
return () => {
|
|
666
|
+
inkInput.off('mouse', onMouse);
|
|
667
|
+
stopEdgeAutoscroll();
|
|
668
|
+
};
|
|
485
669
|
}, [inkInput, isRawModeSupported, store, passthroughCtrlWheelZoom, rows, scrollTranscriptRows, queueScrollCoalesced, applySelectionRect, applySelectionRectThrottled, selectionPointAtCurrentScroll, buildSpanRect]);
|
|
486
670
|
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { useCallback } from 'react';
|
|
13
13
|
import {
|
|
14
14
|
readClipboardImageAttachment,
|
|
15
|
+
readClipboardText,
|
|
15
16
|
readImageAttachmentFromPath,
|
|
16
17
|
splitPastedImagePathCandidates,
|
|
17
18
|
} from '../paste-attachments.mjs';
|
|
@@ -57,36 +58,24 @@ export function usePromptHandlers({
|
|
|
57
58
|
const handlePromptPaste = useCallback((text, meta = {}) => {
|
|
58
59
|
const source = String(meta?.source || 'paste');
|
|
59
60
|
const value = String(text ?? '');
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const hasImagePath = chunks.some((chunk) => chunk.imagePath);
|
|
79
|
-
// No image paths in the paste: fold the whole text into a token when it is
|
|
80
|
-
// large, otherwise let PromptInput insert it raw (return undefined).
|
|
81
|
-
if (!hasImagePath) {
|
|
82
|
-
if (shouldFoldPastedText(value)) return registerPastedText(value);
|
|
83
|
-
return undefined;
|
|
84
|
-
}
|
|
85
|
-
// Mixed paste: resolve each image chunk to an image ref, then fold each
|
|
86
|
-
// CONTIGUOUS run of non-image text into its own token (only if over
|
|
87
|
-
// threshold) so content order around image refs is preserved. '\n'
|
|
88
|
-
// separator chunks are plain text and stay inside their surrounding run.
|
|
89
|
-
return Promise.all(chunks.map(async (chunk) => {
|
|
61
|
+
// Fold-or-insert text pipeline shared by bracketed paste and the Ctrl+V text
|
|
62
|
+
// path. returnRaw=true makes the "insert raw" case return the string itself
|
|
63
|
+
// (needed by the clipboard-text path, whose outer `text` is empty so the
|
|
64
|
+
// handleExternalPaste fallback would insert nothing).
|
|
65
|
+
const processText = (raw, returnRaw = false) => {
|
|
66
|
+
const chunks = splitPastedImagePathCandidates(raw);
|
|
67
|
+
const hasImagePath = chunks.some((chunk) => chunk.imagePath);
|
|
68
|
+
// No image paths: fold the whole text into a token when large, otherwise
|
|
69
|
+
// insert it raw (return undefined → PromptInput inserts, or the string).
|
|
70
|
+
if (!hasImagePath) {
|
|
71
|
+
if (shouldFoldPastedText(raw)) return registerPastedText(raw);
|
|
72
|
+
return returnRaw ? raw : undefined;
|
|
73
|
+
}
|
|
74
|
+
// Mixed paste: resolve each image chunk to an image ref, then fold each
|
|
75
|
+
// CONTIGUOUS run of non-image text into its own token (only if over
|
|
76
|
+
// threshold) so content order around image refs is preserved. '\n'
|
|
77
|
+
// separator chunks are plain text and stay inside their surrounding run.
|
|
78
|
+
return Promise.all(chunks.map(async (chunk) => {
|
|
90
79
|
if (!chunk.imagePath) return chunk.text;
|
|
91
80
|
try {
|
|
92
81
|
const image = await readImageAttachmentFromPath(chunk.text, state.cwd || process.cwd());
|
|
@@ -117,6 +106,36 @@ export function usePromptHandlers({
|
|
|
117
106
|
flushRun();
|
|
118
107
|
return out;
|
|
119
108
|
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Ctrl+V / Meta+V: opencode clipboard.read() model. Prefer OS-clipboard TEXT
|
|
112
|
+
// (routed through the SAME fold pipeline as bracketed paste); when the
|
|
113
|
+
// clipboard holds no text, fall back to the image-attachment path. The async
|
|
114
|
+
// read result is applied by handleExternalPaste under its pasteGeneration
|
|
115
|
+
// staleness guard, so a stale resolve is dropped.
|
|
116
|
+
if (source === 'clipboard-shortcut' && !value) {
|
|
117
|
+
return readClipboardText()
|
|
118
|
+
.then((clip) => {
|
|
119
|
+
const normalized = String(clip ?? '').replace(/\r\n?/g, '\n');
|
|
120
|
+
if (normalized) return processText(normalized, true);
|
|
121
|
+
return readClipboardImageAttachment()
|
|
122
|
+
.then((image) => {
|
|
123
|
+
if (!image) {
|
|
124
|
+
showPromptHint('no text or image found on clipboard', 'plain');
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
const ref = registerPastedImage(image);
|
|
128
|
+
showPromptHint(`attached ${image.filename || 'clipboard image'}`, 'plain');
|
|
129
|
+
return ref;
|
|
130
|
+
});
|
|
131
|
+
})
|
|
132
|
+
.catch((e) => {
|
|
133
|
+
showPromptHint(`paste failed: ${e?.message || e}`, 'warn');
|
|
134
|
+
return false;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return processText(value);
|
|
120
139
|
}, [registerPastedImage, registerPastedText, showPromptHint, state.cwd]);
|
|
121
140
|
|
|
122
141
|
const handlePromptHistoryNavigate = useCallback((direction, currentText = '', meta = {}) => {
|