mixdog 0.9.140 → 0.9.142
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/LICENSES/MIT.txt +4 -0
- package/NOTICE.md +12 -46
- package/package.json +1 -1
- package/scripts/manifest-cache-key.mjs +44 -0
- package/scripts/native-binary-arch.mjs +93 -0
- package/scripts/native-binary-arch.test.mjs +75 -0
- package/scripts/native-tool-download.mjs +118 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +12 -12
- package/src/runtime/agent/orchestrator/session/compact/file-reattach.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +6 -7
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +2 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +1 -1
- package/src/runtime/channels/lib/parent-bridge.mjs +2 -2
- package/src/runtime/channels/lib/runtime-paths.mjs +1 -1
- package/src/standalone/agent-tool/shard-spread.mjs +5 -3
- package/src/standalone/agent-tool/shard-spread.test.mjs +16 -1
- package/src/tui/App.jsx +3 -3
- package/src/tui/app/transcript-window.mjs +4 -5
- package/src/tui/app/use-mouse-input.mjs +2 -2
- package/src/tui/app/use-transcript-scroll.mjs +37 -29
- package/src/tui/components/tool-output-format.mjs +2 -2
- package/src/tui/dist/index.mjs +21 -31
- package/src/tui/themes/teal.mjs +1 -1
- package/vendor/ink/build/ink.js +7 -85
- package/vendor/ink/build/output.js +7 -9
- package/vendor/ink/build/selection-rect.js +80 -0
- package/vendor/ink/build/termio-keypress.js +110 -79
- package/vendor/ink/build/_baseline-termio-keypress.js +0 -240
- package/vendor/ink/build/_baseline-termio-tokenize.js +0 -253
|
@@ -20,9 +20,8 @@ function snapshotBodyWasReturnedByRead(snapshot) {
|
|
|
20
20
|
|| source.startsWith('apply_patch_');
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
// BOM-only read-encoding detection
|
|
24
|
-
//
|
|
25
|
-
// detectFileEncoding. STRICTLY a leading-BOM rule — no content sniffing
|
|
23
|
+
// BOM-only read-encoding detection: buffer[0]===0xff && buffer[1]===0xfe
|
|
24
|
+
// means 'utf16le', and so on. STRICTLY a leading-BOM rule — no content sniffing
|
|
26
25
|
// and no heuristic fallback.
|
|
27
26
|
// Returns the decoder name plus the BOM byte length to strip before
|
|
28
27
|
// decoding. utf8-with-BOM (EF BB BF) keeps the utf-8 decoder; its leading
|
|
@@ -170,7 +170,7 @@ export async function extractIpynbText(fullPath, { maxOutputBytes = DEFAULT_READ
|
|
|
170
170
|
const rawTxt = data['text/plain']
|
|
171
171
|
? (Array.isArray(data['text/plain']) ? data['text/plain'].join('') : data['text/plain'])
|
|
172
172
|
: (Array.isArray(out.text) ? out.text.join('') : out.text);
|
|
173
|
-
//
|
|
173
|
+
// A single huge output is replaced
|
|
174
174
|
// with a jq hint rather than dumped inline.
|
|
175
175
|
if (typeof rawTxt === 'string' && rawTxt.length > IPYNB_OUTPUT_MAX_CHARS) {
|
|
176
176
|
block += `\n# Output: [large output omitted — ${rawTxt.length} chars; inspect with: cat "${fullPath}" | jq '.cells[${cellIndex}].outputs']`;
|
|
@@ -116,7 +116,7 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
116
116
|
_recordReadSnapshot,
|
|
117
117
|
READ_MAX_OUTPUT_BYTES,
|
|
118
118
|
} = helpers;
|
|
119
|
-
//
|
|
119
|
+
// `file_path` alias — the official SDK schema uses `file_path`;
|
|
120
120
|
// mixdog has historically used `path`. Honor `file_path` so a
|
|
121
121
|
// CC-trained agent's call shape works without translation.
|
|
122
122
|
const usedFilePathAlias = typeof args.file_path === 'string' && !args.path;
|
|
@@ -232,8 +232,8 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
232
232
|
// result is sliced back into the original per-entry windows
|
|
233
233
|
// for response assembly. Non-same-path entries are untouched.
|
|
234
234
|
const rawEntries = args.path.map((r) => {
|
|
235
|
-
//
|
|
236
|
-
// 1-based (
|
|
235
|
+
// `file_path` alias on a per-entry batch: file_path is
|
|
236
|
+
// 1-based (SDK schema), so decrement a positive offset to
|
|
237
237
|
// match the 0-based `path` form. Mirrors the scalar
|
|
238
238
|
// alias adjustment at line 57.
|
|
239
239
|
const entryUsesFilePathAlias = typeof r?.file_path === 'string' && !r?.path;
|
|
@@ -599,7 +599,7 @@ export function execShellCommand({
|
|
|
599
599
|
if (grace.unref) grace.unref();
|
|
600
600
|
};
|
|
601
601
|
child.once('exit', _onChildExit);
|
|
602
|
-
// Auto-background transition
|
|
602
|
+
// Auto-background transition. Two triggers
|
|
603
603
|
// resolve the call immediately with a 'backgrounded' result while the
|
|
604
604
|
// child keeps running, adopted into the shell-jobs registry but still
|
|
605
605
|
// owned by this CLI process:
|
|
@@ -904,7 +904,7 @@ export function execShellCommand({
|
|
|
904
904
|
if (abortSignal) {
|
|
905
905
|
abortHandler = () => {
|
|
906
906
|
// Interrupt (the user typed a NEW message while this was running) is a
|
|
907
|
-
// "also look at this" signal, not "stop that" —
|
|
907
|
+
// "also look at this" signal, not "stop that" — it backgrounds instead
|
|
908
908
|
// of killing there, and throwing away a long build the user never asked
|
|
909
909
|
// to stop is the worse outcome. Explicit cancellation (ESC) keeps the
|
|
910
910
|
// kill. The promotion reuses the timeout path's guards verbatim so a
|
|
@@ -337,7 +337,7 @@ export class TaskOutput {
|
|
|
337
337
|
}
|
|
338
338
|
|
|
339
339
|
// Direct mode has no JS write path, so byte counters must come from the
|
|
340
|
-
// filesystem (
|
|
340
|
+
// filesystem (a "poll the file tail" approach).
|
|
341
341
|
_refreshDirectSizes() {
|
|
342
342
|
if (!this.direct) return;
|
|
343
343
|
try { this.stdoutFileSize = statSync(this.stdoutPath).size; } catch {}
|
|
@@ -531,7 +531,7 @@ export class ExecResult {
|
|
|
531
531
|
// renderer in builtin/bash-tool.mjs turns this into a model-visible marker.
|
|
532
532
|
this.failurePhase = opts.failurePhase || null;
|
|
533
533
|
this.failureReason = opts.failureReason || null;
|
|
534
|
-
// Auto-background transition
|
|
534
|
+
// Auto-background transition. When a
|
|
535
535
|
// foreground command outlives autoBackgroundMs the call settles with
|
|
536
536
|
// backgrounded:true + the jobId for manual task control. The
|
|
537
537
|
// child stays owned by the CLI process; stdout/stderr keep flowing to
|
|
@@ -55,7 +55,7 @@ const BLOCKED_PATTERNS = [
|
|
|
55
55
|
// cmd `del /s` / `rd /s`) are NOT blocked outright — each is target-checked
|
|
56
56
|
// by a dedicated guard below via _isDangerousDeleteTarget, blocking only
|
|
57
57
|
// filesystem-root / home / top-level-system / whole-cwd targets (CC-level).
|
|
58
|
-
// `git reset --hard` is likewise no longer hard-blocked (
|
|
58
|
+
// `git reset --hard` is likewise no longer hard-blocked (a prompt suffices; the
|
|
59
59
|
// agent workflow already gates destructive git ops).
|
|
60
60
|
// Bare `git push --force` (and `--force=`) still blocks; the safer
|
|
61
61
|
// `--force-with-lease` / `--force-if-includes` variants pass.
|
|
@@ -28,11 +28,11 @@ function setChannelNotifySink(fn) {
|
|
|
28
28
|
|
|
29
29
|
function createParentBridge({ getInstanceId, ipcProcess = process }) {
|
|
30
30
|
function sendNotifyToParent(method, params) {
|
|
31
|
-
//
|
|
31
|
+
// The channel schema requires meta: Record<string,string>.
|
|
32
32
|
// Coerce every meta value to string so a non-string (e.g. a Discord
|
|
33
33
|
// interaction.type number) can't fail zod and silently drop the notify.
|
|
34
34
|
// silent_to_agent stays boolean — an internal routing flag the daemon
|
|
35
|
-
// router / agentNotify consume (=== true) before the
|
|
35
|
+
// router / agentNotify consume (=== true) before the zod boundary.
|
|
36
36
|
const outParams = normalizeChannelNotifyParams(method, params);
|
|
37
37
|
if (_notifySink) {
|
|
38
38
|
try { _notifySink(method, outParams); }
|
|
@@ -289,7 +289,7 @@ function refreshActiveInstance(instanceId, meta, options) {
|
|
|
289
289
|
}
|
|
290
290
|
preservedExtra.gateway_server_pid = prevGatewayServerPid;
|
|
291
291
|
// Clear session-scoped gateway metrics when the transcript changes —
|
|
292
|
-
// a new
|
|
292
|
+
// a new session must not inherit the previous session's context
|
|
293
293
|
// usage before the gateway re-advertises.
|
|
294
294
|
const metricTranscript =
|
|
295
295
|
typeof prevForPreserve?.gateway_transcript_path === 'string' && prevForPreserve.gateway_transcript_path
|
|
@@ -611,9 +611,11 @@ export function createAgentShardSpread({ mgr, log = null } = {}) {
|
|
|
611
611
|
const createRemoteSession = createRemoteAgentSession;
|
|
612
612
|
const remote = (id) => handles.get(String(id || '')) || null;
|
|
613
613
|
|
|
614
|
-
// mgr
|
|
615
|
-
//
|
|
616
|
-
|
|
614
|
+
// mgr is normally an ESM module namespace. Copy its enumerable exports onto
|
|
615
|
+
// a writable facade before overriding the session-addressed methods. Using
|
|
616
|
+
// the namespace as a prototype makes assignment hit its immutable exports
|
|
617
|
+
// under Node 24 (Electron 41), so even `wrapped.getSession = ...` throws.
|
|
618
|
+
const wrapped = Object.assign(Object.create(null), mgr);
|
|
617
619
|
wrapped.getSession = (id) => {
|
|
618
620
|
const handle = remote(id);
|
|
619
621
|
return handle ? handle.facade : mgr.getSession(id);
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import test from 'node:test';
|
|
3
3
|
|
|
4
|
-
import { remoteTurnHandoffReady } from './shard-spread.mjs';
|
|
4
|
+
import { createAgentShardSpread, remoteTurnHandoffReady } from './shard-spread.mjs';
|
|
5
|
+
|
|
6
|
+
test('manager wrapper owns writable overrides when the source manager is immutable', () => {
|
|
7
|
+
const localSession = { id: 'local-session' };
|
|
8
|
+
const createSession = () => localSession;
|
|
9
|
+
const mgr = Object.freeze({
|
|
10
|
+
createSession,
|
|
11
|
+
getSession: (id) => (id === localSession.id ? localSession : null),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const spread = createAgentShardSpread({ mgr });
|
|
15
|
+
|
|
16
|
+
assert.equal(Object.hasOwn(spread.mgr, 'getSession'), true);
|
|
17
|
+
assert.equal(spread.mgr.getSession(localSession.id), localSession);
|
|
18
|
+
assert.equal(spread.mgr.createSession, createSession);
|
|
19
|
+
});
|
|
5
20
|
|
|
6
21
|
test('remote turn waits through the turndone-to-save gap for assistant content', () => {
|
|
7
22
|
assert.equal(remoteTurnHandoffReady({
|
package/src/tui/App.jsx
CHANGED
|
@@ -104,7 +104,7 @@ import {
|
|
|
104
104
|
TRANSCRIPT_MEASURED_ROWS,
|
|
105
105
|
selectionRectsEqual,
|
|
106
106
|
shiftSelectionRectY,
|
|
107
|
-
|
|
107
|
+
compareCellOrder,
|
|
108
108
|
upperBound,
|
|
109
109
|
resolveAnchorScrollOffset,
|
|
110
110
|
transcriptItemVariantKey,
|
|
@@ -641,8 +641,8 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
|
|
|
641
641
|
// | null. Press decides it; motion/release stay in that region.
|
|
642
642
|
// anchorSpan: for word/line multi-click selections, the initial word/line
|
|
643
643
|
// bounds ({ lo:{x,y}, hi:{x,y}, kind:'word'|'line' }) so a subsequent drag
|
|
644
|
-
// extends the selection whole-word/whole-line from that span
|
|
645
|
-
//
|
|
644
|
+
// extends the selection whole-word/whole-line from that span. Null ⇔ an
|
|
645
|
+
// ordinary char-drag selection.
|
|
646
646
|
const dragRef = useRef({ anchor: null, anchorScroll: 0, last: null, active: false, rect: null, region: null, anchorSpan: null });
|
|
647
647
|
const transcriptViewportRef = useRef({ top: 0, bottom: 0 });
|
|
648
648
|
const panelTransitionRef = useRef({ signature: '', reserve: 0, clearRows: 0, guardRows: 0, epoch: 0 });
|
|
@@ -135,11 +135,10 @@ export function shiftSelectionRectY(rect, deltaY) {
|
|
|
135
135
|
return { ...rect, y1: rect.y1 + dy, y2: rect.y2 + dy };
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
// Reading
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return 0;
|
|
138
|
+
// Reading order over grid cells: the row decides, the column breaks ties.
|
|
139
|
+
// Returns the sign of the difference, so it drops straight into comparisons.
|
|
140
|
+
export function compareCellOrder(a, b) {
|
|
141
|
+
return a.y === b.y ? Math.sign(a.x - b.x) : Math.sign(a.y - b.y);
|
|
143
142
|
}
|
|
144
143
|
|
|
145
144
|
|
|
@@ -590,7 +590,7 @@ export function useMouseInput({
|
|
|
590
590
|
const now = Date.now();
|
|
591
591
|
// Shift+click on an existing word/line (anchorSpan) selection extends
|
|
592
592
|
// that selection by whole words/lines to the click point, preserving
|
|
593
|
-
// the original anchor span
|
|
593
|
+
// the original anchor span.
|
|
594
594
|
if (
|
|
595
595
|
extendHeld
|
|
596
596
|
&& dragRef.current.region === region
|
|
@@ -660,7 +660,7 @@ export function useMouseInput({
|
|
|
660
660
|
// words/lines from this span (see buildSpanRect). Leave the drag
|
|
661
661
|
// ARMED (active:true): a release without motion keeps this highlight
|
|
662
662
|
// (buildSpanRect returns the span for an in-span target), while
|
|
663
|
-
// any motion extends it.
|
|
663
|
+
// any motion extends it. The word/line select sets
|
|
664
664
|
// isDragging=true + anchorSpan; the mouse-up finalizes.
|
|
665
665
|
const kind = clickCount === 2 ? 'word' : 'line';
|
|
666
666
|
const wr = kind === 'word' ? store.getWordRectAt?.(x, y) : store.getLineRectAt?.(y);
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
accumulateDirectionalScrollDelta,
|
|
17
17
|
selectionRectsEqual,
|
|
18
18
|
shiftSelectionRectY,
|
|
19
|
-
|
|
19
|
+
compareCellOrder,
|
|
20
20
|
upperBound,
|
|
21
21
|
transcriptRowAt,
|
|
22
22
|
} from './transcript-window.mjs';
|
|
@@ -101,7 +101,7 @@ export function useTranscriptScroll({
|
|
|
101
101
|
// Synchronous sibling of harvestStitchRowsSoon: snapshot the rows CURRENTLY
|
|
102
102
|
// under the selection into the stitch buffer immediately, keyed by the given
|
|
103
103
|
// (pre-scroll) offset. Called right before a scroll shifts those rows out of
|
|
104
|
-
// view —
|
|
104
|
+
// view — the harvest has to grab the outgoing
|
|
105
105
|
// rows BEFORE scrollBy overwrites them. The deferred harvest could never see
|
|
106
106
|
// rows that a fast drag/wheel scrolled past between paint and its setTimeout.
|
|
107
107
|
// selectionRows is harvested by the renderer UNCONDITIONALLY (even on the
|
|
@@ -373,35 +373,43 @@ export function useTranscriptScroll({
|
|
|
373
373
|
};
|
|
374
374
|
}, []);
|
|
375
375
|
|
|
376
|
-
//
|
|
377
|
-
// component scope so BOTH the mouse
|
|
378
|
-
// auto-scroll path
|
|
379
|
-
//
|
|
380
|
-
// the
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
// at the cursor; a miss (blank/gutter) falls back to the raw cell. spanScroll
|
|
384
|
-
// re-anchors the span to the current transcript scroll (status never scrolls) so
|
|
385
|
-
// the original word/line tracks the content while dragging/auto-scrolling.
|
|
376
|
+
// Grow a word/line multi-click selection from its anchor span out to the
|
|
377
|
+
// word/line under the cursor. Hoisted to component scope so BOTH the mouse
|
|
378
|
+
// handler (motion/release) AND the auto-scroll path can rebuild a span-aware
|
|
379
|
+
// rect. The moving end snaps to the word or line under the cursor and falls
|
|
380
|
+
// back to the raw cell on a miss; spanScroll re-anchors the stored span to
|
|
381
|
+
// the current transcript scroll (the status band never scrolls) so the
|
|
382
|
+
// original word keeps tracking its content while dragging or auto-scrolling.
|
|
386
383
|
const buildSpanRect = useCallback((span, x, y, region, spanScroll = 0) => {
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
384
|
+
const atCurrentScroll = (point) => (
|
|
385
|
+
region === 'status' ? point : selectionPointAtCurrentScroll(point, spanScroll)
|
|
386
|
+
);
|
|
387
|
+
const anchorStart = atCurrentScroll(span.lo);
|
|
388
|
+
const anchorEnd = atCurrentScroll(span.hi);
|
|
389
|
+
|
|
390
|
+
const snapped = span.kind === 'word'
|
|
391
|
+
? store.getWordRectAt?.(x, y)
|
|
392
|
+
: store.getLineRectAt?.(y);
|
|
393
|
+
let targetStart;
|
|
394
|
+
let targetEnd;
|
|
395
|
+
if (snapped) {
|
|
396
|
+
targetStart = { x: snapped.x1, y: snapped.y1 };
|
|
397
|
+
targetEnd = { x: snapped.x2, y: snapped.y2 };
|
|
398
|
+
} else if (span.kind === 'word') {
|
|
399
|
+
targetStart = { x, y };
|
|
400
|
+
targetEnd = { x, y };
|
|
396
401
|
} else {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
else { mLo = { x: 0, y }; mHi = { x: Math.max(0, frameColumns - 1), y }; }
|
|
402
|
+
targetStart = { x: 0, y };
|
|
403
|
+
targetEnd = { x: Math.max(0, frameColumns - 1), y };
|
|
400
404
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
+
|
|
406
|
+
// The anchor span always stays whole; the rect reaches from it toward the
|
|
407
|
+
// target when the target sits clear of it on either side, and collapses
|
|
408
|
+
// back to the anchor when the two overlap.
|
|
409
|
+
const linear = (from, to) => ({ mode: 'linear', x1: from.x, y1: from.y, x2: to.x, y2: to.y });
|
|
410
|
+
if (compareCellOrder(targetEnd, anchorStart) < 0) return linear(anchorEnd, targetStart);
|
|
411
|
+
if (compareCellOrder(targetStart, anchorEnd) > 0) return linear(anchorStart, targetEnd);
|
|
412
|
+
return linear(anchorStart, anchorEnd);
|
|
405
413
|
}, [store, frameColumns, selectionPointAtCurrentScroll]);
|
|
406
414
|
|
|
407
415
|
const transcriptViewportRows = useCallback(() => {
|
|
@@ -538,7 +546,7 @@ export function useTranscriptScroll({
|
|
|
538
546
|
}
|
|
539
547
|
// Before the scroll moves selected rows out of view, snapshot the rows
|
|
540
548
|
// currently under the selection into the stitch buffer keyed by the
|
|
541
|
-
// PRE-scroll offset
|
|
549
|
+
// PRE-scroll offset. Runs for BOTH
|
|
542
550
|
// an active drag and a wheel-shift of a released selection, so Ctrl+C
|
|
543
551
|
// reconstructs the full text no matter how far it scrolled off-screen.
|
|
544
552
|
if (appliedDelta !== 0 && dragRef.current.region === 'transcript' && dragRef.current.rect) {
|
|
@@ -35,11 +35,11 @@ import { buildTableRender } from '../markdown/table-layout.mjs';
|
|
|
35
35
|
const DEFAULT_MARKDOWN_WIDTH = 80;
|
|
36
36
|
|
|
37
37
|
// Hard ceilings so a pathological tool result can never lock the render loop.
|
|
38
|
-
//
|
|
38
|
+
// The collapsed fold budgets roughly MAX_LINES*width*4; for the EXPANDED body we
|
|
39
39
|
// cap total characters processed and total lines kept, with an explicit marker.
|
|
40
40
|
const MAX_EXPANDED_CHARS = 256 * 1024; // 256 KB of text gets per-line processing
|
|
41
41
|
const MAX_EXPANDED_LINES = 4000; // logical-line safety ceiling; physical mount cap is separate
|
|
42
|
-
const MAX_JSON_FORMAT_LENGTH = 10_000; //
|
|
42
|
+
const MAX_JSON_FORMAT_LENGTH = 10_000; // JSON pretty-format cap
|
|
43
43
|
const MAX_HIGHLIGHT_LINE_CHARS = 2000; // skip token-scan on absurdly long lines
|
|
44
44
|
// Lockstep with ToolExecution collapsed fit budget (MIN_RESULT_LINE_CHARS).
|
|
45
45
|
const MIN_EXPANDED_BODY_COLS = 24;
|
package/src/tui/dist/index.mjs
CHANGED
|
@@ -2061,7 +2061,7 @@ var tealPalette = {
|
|
|
2061
2061
|
error: "rgb(204,102,102)",
|
|
2062
2062
|
// red #cc6666
|
|
2063
2063
|
warning: "rgb(255,255,0)",
|
|
2064
|
-
// yellow #ffff00 (
|
|
2064
|
+
// yellow #ffff00 (pure yellow by design)
|
|
2065
2065
|
suggestion: "rgb(129,162,190)",
|
|
2066
2066
|
// mdLink #81a2be
|
|
2067
2067
|
panelTitle: "rgb(138,190,183)",
|
|
@@ -12937,10 +12937,8 @@ function shiftSelectionRectY(rect, deltaY) {
|
|
|
12937
12937
|
if (!rect || dy === 0) return rect || null;
|
|
12938
12938
|
return { ...rect, y1: rect.y1 + dy, y2: rect.y2 + dy };
|
|
12939
12939
|
}
|
|
12940
|
-
function
|
|
12941
|
-
|
|
12942
|
-
if (a.x !== b.x) return a.x < b.x ? -1 : 1;
|
|
12943
|
-
return 0;
|
|
12940
|
+
function compareCellOrder(a, b) {
|
|
12941
|
+
return a.y === b.y ? Math.sign(a.x - b.x) : Math.sign(a.y - b.y);
|
|
12944
12942
|
}
|
|
12945
12943
|
function lowerBound(values, target) {
|
|
12946
12944
|
let lo = 0;
|
|
@@ -14173,34 +14171,26 @@ function useTranscriptScroll({
|
|
|
14173
14171
|
};
|
|
14174
14172
|
}, []);
|
|
14175
14173
|
const buildSpanRect = useCallback3((span, x, y, region, spanScroll = 0) => {
|
|
14176
|
-
const
|
|
14177
|
-
const
|
|
14178
|
-
const
|
|
14179
|
-
|
|
14180
|
-
let
|
|
14181
|
-
|
|
14182
|
-
|
|
14183
|
-
|
|
14184
|
-
|
|
14185
|
-
|
|
14186
|
-
|
|
14187
|
-
|
|
14188
|
-
mHi = { x, y };
|
|
14189
|
-
}
|
|
14174
|
+
const atCurrentScroll = (point) => region === "status" ? point : selectionPointAtCurrentScroll(point, spanScroll);
|
|
14175
|
+
const anchorStart = atCurrentScroll(span.lo);
|
|
14176
|
+
const anchorEnd = atCurrentScroll(span.hi);
|
|
14177
|
+
const snapped = span.kind === "word" ? store.getWordRectAt?.(x, y) : store.getLineRectAt?.(y);
|
|
14178
|
+
let targetStart;
|
|
14179
|
+
let targetEnd;
|
|
14180
|
+
if (snapped) {
|
|
14181
|
+
targetStart = { x: snapped.x1, y: snapped.y1 };
|
|
14182
|
+
targetEnd = { x: snapped.x2, y: snapped.y2 };
|
|
14183
|
+
} else if (span.kind === "word") {
|
|
14184
|
+
targetStart = { x, y };
|
|
14185
|
+
targetEnd = { x, y };
|
|
14190
14186
|
} else {
|
|
14191
|
-
|
|
14192
|
-
|
|
14193
|
-
mLo = { x: lr.x1, y: lr.y1 };
|
|
14194
|
-
mHi = { x: lr.x2, y: lr.y2 };
|
|
14195
|
-
} else {
|
|
14196
|
-
mLo = { x: 0, y };
|
|
14197
|
-
mHi = { x: Math.max(0, frameColumns - 1), y };
|
|
14198
|
-
}
|
|
14187
|
+
targetStart = { x: 0, y };
|
|
14188
|
+
targetEnd = { x: Math.max(0, frameColumns - 1), y };
|
|
14199
14189
|
}
|
|
14200
|
-
const
|
|
14201
|
-
if (
|
|
14202
|
-
if (
|
|
14203
|
-
return
|
|
14190
|
+
const linear = (from, to) => ({ mode: "linear", x1: from.x, y1: from.y, x2: to.x, y2: to.y });
|
|
14191
|
+
if (compareCellOrder(targetEnd, anchorStart) < 0) return linear(anchorEnd, targetStart);
|
|
14192
|
+
if (compareCellOrder(targetStart, anchorEnd) > 0) return linear(anchorStart, targetEnd);
|
|
14193
|
+
return linear(anchorStart, anchorEnd);
|
|
14204
14194
|
}, [store, frameColumns, selectionPointAtCurrentScroll]);
|
|
14205
14195
|
const transcriptViewportRows = useCallback3(() => {
|
|
14206
14196
|
const top = Math.max(0, Number(transcriptViewportRef.current?.top) || 0);
|
package/src/tui/themes/teal.mjs
CHANGED
|
@@ -38,7 +38,7 @@ export const tealPalette = {
|
|
|
38
38
|
promptBorder: 'rgb(80,80,80)', // borderMuted/darkGray #505050
|
|
39
39
|
success: 'rgb(181,189,104)', // green #b5bd68
|
|
40
40
|
error: 'rgb(204,102,102)', // red #cc6666
|
|
41
|
-
warning: 'rgb(255,255,0)', // yellow #ffff00 (
|
|
41
|
+
warning: 'rgb(255,255,0)', // yellow #ffff00 (pure yellow by design)
|
|
42
42
|
suggestion: 'rgb(129,162,190)', // mdLink #81a2be
|
|
43
43
|
panelTitle: 'rgb(138,190,183)', // accent
|
|
44
44
|
permission: 'rgb(204,102,102)', // red
|
package/vendor/ink/build/ink.js
CHANGED
|
@@ -14,6 +14,7 @@ import reconciler from './reconciler.js';
|
|
|
14
14
|
import render from './renderer.js';
|
|
15
15
|
import * as dom from './dom.js';
|
|
16
16
|
import { hideCursorEscape, showCursorEscape } from './cursor-helpers.js';
|
|
17
|
+
import { wordRectAt } from './selection-rect.js';
|
|
17
18
|
import logUpdate, { frameLog } from './log-update.js';
|
|
18
19
|
import { bsu, esu, shouldSynchronize } from './write-synchronized.js';
|
|
19
20
|
import instances from './instances.js';
|
|
@@ -451,91 +452,12 @@ export default class Ink {
|
|
|
451
452
|
this.rootNode.onRender();
|
|
452
453
|
});
|
|
453
454
|
};
|
|
454
|
-
// [mixdog fork]
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
|
|
460
|
-
// model) onto mixdog's rect(linear) infra. Instead of a naive "non-space run",
|
|
461
|
-
// expansion stops at a CHARACTER-CLASS change:
|
|
462
|
-
// class 1 = WORD_CHAR — letters (any script), digits, and the punctuation
|
|
463
|
-
// iTerm2 treats as word-part by default (/-+~_.\), so a path like
|
|
464
|
-
// `/usr/bin/bash` or `~/.claude/config.json` selects whole.
|
|
465
|
-
// class 2 = other punctuation — so `->` selects just `->`, not the words
|
|
466
|
-
// on either side.
|
|
467
|
-
// class 0 = space/empty. A space run could be treated as selectable
|
|
468
|
-
// (class 0), but mixdog intentionally returns null on empty/space
|
|
469
|
-
// so a double-click on blank does nothing (safer for our transcript
|
|
470
|
-
// where most alt-screen cells are padding).
|
|
471
|
-
getWordRectAt = (x, y) => {
|
|
472
|
-
const rows = this.lastPlainRows;
|
|
473
|
-
if (!rows)
|
|
474
|
-
return null;
|
|
475
|
-
const cells = rows[y];
|
|
476
|
-
if (!Array.isArray(cells))
|
|
477
|
-
return null;
|
|
478
|
-
// Unicode-aware word-char set (matches selection.ts WORD_CHAR).
|
|
479
|
-
const WORD_CHAR = /[\p{L}\p{N}_/.\-+~\\]/u;
|
|
480
|
-
const charClass = (v) => {
|
|
481
|
-
if (!v || v === ' ')
|
|
482
|
-
return 0;
|
|
483
|
-
return WORD_CHAR.test(v) ? 1 : 2;
|
|
484
|
-
};
|
|
485
|
-
// [mixdog fork] Wide/CJK glyphs occupy 2+ grid cells: the HEAD cell
|
|
486
|
-
// holds the glyph and each TRAILING cell is stored as '' (spacer tail)
|
|
487
|
-
// carrying the glyph's styles — see output.js ~L237-243 for how wide
|
|
488
|
-
// chars are laid into the grid. A '' cell is a wide-char TAIL only when
|
|
489
|
-
// it directly follows a non-empty non-space glyph (class !== 0); a ''
|
|
490
|
-
// after '' or after a space is genuine blank padding. This mirrors
|
|
491
|
-
// selection.ts wordBoundsAt's SpacerTail step-back (L172-178) and
|
|
492
|
-
// expansion step-over (L206-221) on mixdog's string-cell grid.
|
|
493
|
-
const isWideTail = (i) => i > 0 && cells[i] === '' && charClass(cells[i - 1]) !== 0;
|
|
494
|
-
// On entry: if the click landed on a spacer tail, step back to the head
|
|
495
|
-
// so charClass sees the actual glyph. Genuine blank padding is left
|
|
496
|
-
// alone, preserving the null-on-blank behavior below.
|
|
497
|
-
let sx = x;
|
|
498
|
-
if (isWideTail(sx))
|
|
499
|
-
sx = sx - 1;
|
|
500
|
-
const cls = charClass(cells[sx]);
|
|
501
|
-
// Preserve mixdog's null-on-space/empty behavior (class 0).
|
|
502
|
-
if (cls === 0)
|
|
503
|
-
return null;
|
|
504
|
-
let x1 = sx, x2 = sx;
|
|
505
|
-
// Expand left: step OVER a spacer tail to the wide-char head and include
|
|
506
|
-
// both columns when the head matches the class; otherwise stop at a
|
|
507
|
-
// class change.
|
|
508
|
-
while (x1 - 1 >= 0) {
|
|
509
|
-
const p = x1 - 1;
|
|
510
|
-
if (isWideTail(p)) {
|
|
511
|
-
if (p - 1 >= 0 && charClass(cells[p - 1]) === cls) {
|
|
512
|
-
x1 = p - 1;
|
|
513
|
-
continue;
|
|
514
|
-
}
|
|
515
|
-
break;
|
|
516
|
-
}
|
|
517
|
-
if (charClass(cells[p]) === cls) {
|
|
518
|
-
x1 = p;
|
|
519
|
-
continue;
|
|
520
|
-
}
|
|
521
|
-
break;
|
|
522
|
-
}
|
|
523
|
-
// Expand right: INCLUDE a spacer tail that follows an in-run glyph so x2
|
|
524
|
-
// covers the wide glyph's full width; otherwise stop at a class change.
|
|
525
|
-
while (x2 + 1 < cells.length) {
|
|
526
|
-
const n = x2 + 1;
|
|
527
|
-
if (isWideTail(n)) {
|
|
528
|
-
x2 = n;
|
|
529
|
-
continue;
|
|
530
|
-
}
|
|
531
|
-
if (charClass(cells[n]) === cls) {
|
|
532
|
-
x2 = n;
|
|
533
|
-
continue;
|
|
534
|
-
}
|
|
535
|
-
break;
|
|
536
|
-
}
|
|
537
|
-
return { x1, y1: y, x2, y2: y };
|
|
538
|
-
};
|
|
455
|
+
// [mixdog fork] Inclusive rect of the word at cell (x, y) on that row, or
|
|
456
|
+
// null when the cell is blank or out of range. Reuses the cached cell-value
|
|
457
|
+
// rows from the last render, so it works without retaining the Output
|
|
458
|
+
// instance; the class model and wide-glyph handling live in
|
|
459
|
+
// selection-rect.js.
|
|
460
|
+
getWordRectAt = (x, y) => wordRectAt(this.lastPlainRows, x, y);
|
|
539
461
|
// [mixdog fork] Given a 0-based row y, return the inclusive rect of the whole
|
|
540
462
|
// logical line at that row (select-line intent on
|
|
541
463
|
// mixdog's rect infra). x1 is always 0; x2 is the last non-space content cell
|
|
@@ -302,15 +302,13 @@ export default class Output {
|
|
|
302
302
|
// text is trailing-space-trimmed and partial first/last rows respect
|
|
303
303
|
// x1/x2. Null when there is no selection (mirrors selectedText).
|
|
304
304
|
let selectionRows = null;
|
|
305
|
-
// [mixdog fork]
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
// implemented. It needs a noSelect bit threaded through the render
|
|
313
|
-
// pipeline before it can be done cleanly.
|
|
305
|
+
// [mixdog fork] Excluding gutter / line-number / diff-sigil cells from
|
|
306
|
+
// the highlight and the copy is deliberately NOT implemented: the Output
|
|
307
|
+
// grid stores only {value, styles} per cell, with no flag distinguishing
|
|
308
|
+
// chrome from content. Inferring gutters from position or content would
|
|
309
|
+
// be a fragile heuristic (line numbers, diff +/- sigils and real content
|
|
310
|
+
// are indistinguishable at the cell level), so this needs an explicit
|
|
311
|
+
// no-select bit threaded through the render pipeline first.
|
|
314
312
|
if (sel) {
|
|
315
313
|
const captureSelectedText = sel.captureText !== false;
|
|
316
314
|
if (!captureSelectedText) {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* selection-rect.js — double-click word bounds over the rendered cell grid.
|
|
3
|
+
*
|
|
4
|
+
* The grid stores one string per column. A wide glyph (CJK, emoji) keeps its
|
|
5
|
+
* text in the HEAD column and leaves the column it overhangs as '' — a
|
|
6
|
+
* continuation cell that carries the head's styling. Everything here works in
|
|
7
|
+
* GLYPH units so a wide character is never split down the middle.
|
|
8
|
+
*
|
|
9
|
+
* Expansion stops at a character-class change rather than at whitespace, which
|
|
10
|
+
* is what terminals do: double-clicking `foo` takes `foo`, `->` takes `->`, and
|
|
11
|
+
* a path stays whole because terminals count a few punctuation marks as
|
|
12
|
+
* word-forming. iTerm2 publishes that default set as / - + ~ _ . and the
|
|
13
|
+
* backslash is added here so Windows paths behave the same way.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const CLASS_BLANK = 0;
|
|
17
|
+
const CLASS_WORD = 1;
|
|
18
|
+
const CLASS_SYMBOL = 2;
|
|
19
|
+
|
|
20
|
+
const WORD_PUNCTUATION = new Set(['_', '/', '.', '-', '+', '~', '\\']);
|
|
21
|
+
const LETTER_OR_DIGIT = /[\p{L}\p{N}]/u;
|
|
22
|
+
|
|
23
|
+
function carriesWordPunctuation(value) {
|
|
24
|
+
for (const character of value) {
|
|
25
|
+
if (WORD_PUNCTUATION.has(character)) return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Blank, word-forming, or plain symbol. Empty and space are both blank. */
|
|
31
|
+
export function cellClass(value) {
|
|
32
|
+
if (!value || value === ' ') return CLASS_BLANK;
|
|
33
|
+
return LETTER_OR_DIGIT.test(value) || carriesWordPunctuation(value)
|
|
34
|
+
? CLASS_WORD
|
|
35
|
+
: CLASS_SYMBOL;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The glyph occupying `column`, as inclusive head/tail columns. A continuation
|
|
40
|
+
* cell is an empty string directly after a non-blank glyph; anything else that
|
|
41
|
+
* is empty — column 0, a run of empties, an empty after a space — is padding
|
|
42
|
+
* and stands alone.
|
|
43
|
+
*/
|
|
44
|
+
function glyphAt(cells, column) {
|
|
45
|
+
const continues = (index) => index > 0
|
|
46
|
+
&& cells[index] === ''
|
|
47
|
+
&& cellClass(cells[index - 1]) !== CLASS_BLANK;
|
|
48
|
+
const head = continues(column) ? column - 1 : column;
|
|
49
|
+
return { head, tail: continues(head + 1) ? head + 1 : head };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Inclusive rect of the word at (x, y), or null when the cell is blank or out
|
|
54
|
+
* of range. Blank cells deliberately yield nothing so a double-click on the
|
|
55
|
+
* padding around transcript content does not select a run of spaces.
|
|
56
|
+
*/
|
|
57
|
+
export function wordRectAt(rows, x, y) {
|
|
58
|
+
const cells = Array.isArray(rows?.[y]) ? rows[y] : null;
|
|
59
|
+
if (!cells) return null;
|
|
60
|
+
|
|
61
|
+
const origin = glyphAt(cells, x);
|
|
62
|
+
const wordClass = cellClass(cells[origin.head]);
|
|
63
|
+
if (wordClass === CLASS_BLANK) return null;
|
|
64
|
+
|
|
65
|
+
let first = origin.head;
|
|
66
|
+
while (first > 0) {
|
|
67
|
+
const previous = glyphAt(cells, first - 1);
|
|
68
|
+
if (cellClass(cells[previous.head]) !== wordClass) break;
|
|
69
|
+
first = previous.head;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let last = origin.tail;
|
|
73
|
+
while (last + 1 < cells.length) {
|
|
74
|
+
const next = glyphAt(cells, last + 1);
|
|
75
|
+
if (cellClass(cells[next.head]) !== wordClass) break;
|
|
76
|
+
last = next.tail;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { x1: first, y1: y, x2: last, y2: y };
|
|
80
|
+
}
|