mixdog 0.9.140 → 0.9.141
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/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +3 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +2 -3
- package/src/runtime/channels/lib/parent-bridge.mjs +2 -2
- 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 +1 -1
- package/src/tui/app/use-transcript-scroll.mjs +37 -29
- package/src/tui/dist/index.mjs +20 -30
- 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
package/LICENSES/MIT.txt
CHANGED
|
@@ -12,6 +12,10 @@ component listed here. Each component keeps its own copyright notice.
|
|
|
12
12
|
https://github.com/microsoft/vscode-codicons
|
|
13
13
|
Copyright (c) Microsoft Corporation
|
|
14
14
|
|
|
15
|
+
Octicons
|
|
16
|
+
https://github.com/primer/octicons
|
|
17
|
+
Copyright (c) GitHub, Inc.
|
|
18
|
+
|
|
15
19
|
GitHub Pull Requests and Issues (VS Code extension)
|
|
16
20
|
https://github.com/microsoft/vscode-pull-request-github
|
|
17
21
|
Copyright (c) Microsoft Corporation. All rights reserved.
|
package/NOTICE.md
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
# NOTICE
|
|
2
2
|
|
|
3
|
-
Mixdog itself is MIT-licensed (see `LICENSE`). The
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
implementation, with no code taken. Everything is kept here in one place so
|
|
7
|
-
the individual source files stay free of scattered attribution comments.
|
|
3
|
+
Mixdog itself is MIT-licensed (see `LICENSE`). The sections below cover the
|
|
4
|
+
third-party code and data that Mixdog actually carries, kept here in one place
|
|
5
|
+
so the individual source files stay free of scattered attribution comments.
|
|
8
6
|
|
|
9
7
|
Full license texts live in `LICENSES/`:
|
|
10
8
|
|
|
@@ -47,6 +45,15 @@ The chrome-level UI glyph font used by the desktop and web renderer. The code
|
|
|
47
45
|
is MIT; the icons themselves are licensed under Creative Commons Attribution
|
|
48
46
|
4.0 International (CC BY 4.0).
|
|
49
47
|
|
|
48
|
+
### Octicons — Copyright (c) GitHub, Inc.
|
|
49
|
+
|
|
50
|
+
<https://github.com/primer/octicons>
|
|
51
|
+
|
|
52
|
+
The five changed-file status glyphs in
|
|
53
|
+
`apps/desktop/src/renderer/ScmStatusIcon.tsx` carry Octicons 16px path data
|
|
54
|
+
unmodified: `diffAdded`, `diffModified`, `diffRemoved`, `diffRenamed` and
|
|
55
|
+
`alert`.
|
|
56
|
+
|
|
50
57
|
### GitHub Pull Requests extension — Copyright (c) Microsoft Corporation
|
|
51
58
|
|
|
52
59
|
<https://github.com/microsoft/vscode-pull-request-github>
|
|
@@ -98,44 +105,3 @@ upstream NOTICE file is preserved as `LICENSES/codex-NOTICE.txt`.
|
|
|
98
105
|
The Apache-2.0 terms require this notice to travel with any redistribution of
|
|
99
106
|
the derived files. Full license text: `LICENSES/Apache-2.0.txt`
|
|
100
107
|
(<https://www.apache.org/licenses/LICENSE-2.0>).
|
|
101
|
-
|
|
102
|
-
## Behavioral references
|
|
103
|
-
|
|
104
|
-
The projects below informed Mixdog through publicly observable behavior, a
|
|
105
|
-
published wire contract, or a documented algorithm. No source code from them
|
|
106
|
-
is present in Mixdog, so no license obligation travels with the result; they
|
|
107
|
-
are recorded here because the source files themselves carry no attribution
|
|
108
|
-
comments.
|
|
109
|
-
|
|
110
|
-
- **Chromium** — the workspace tab strip follows Chromium's tab-strip layout
|
|
111
|
-
semantics: two layout domains around the crossover width, the active-tab
|
|
112
|
-
floor, the inactive sliver floor, the left-to-right remainder grant and the
|
|
113
|
-
icon visibility ladder. The implementation is independent TypeScript and CSS
|
|
114
|
-
on Mixdog's own constants (`apps/desktop/src/renderer/WorkspaceTabStrip.tsx`,
|
|
115
|
-
`apps/desktop/src/renderer/desktop/04-workspace-tabs.css`).
|
|
116
|
-
- **GitHub Desktop** — the Source Control dock grammar: commit-form layout,
|
|
117
|
-
changed-file status glyph semantics, single-sentence path colouring, history
|
|
118
|
-
context-menu gating, and detached-checkout / tag rules. Painted entirely on
|
|
119
|
-
Mixdog's own semantic tokens.
|
|
120
|
-
- **OpenCode** — transcript auto-scroll gesture grammar, virtual-timeline
|
|
121
|
-
anchoring and the streaming-markdown projection model.
|
|
122
|
-
- **Orca** — editor-surface and tab-hierarchy structure, the terminal host
|
|
123
|
-
portal, the keep-awake power-save blocker and the hosted-review link
|
|
124
|
-
derivation.
|
|
125
|
-
- **Files** — the folder pane's grouping keys, date-span labels, size buckets
|
|
126
|
-
and discrete layout size ladder
|
|
127
|
-
(`apps/desktop/src/renderer/FolderPane.lazy.tsx`).
|
|
128
|
-
- **Claude Code** (Anthropic) — terminal input tokenizing and keypress
|
|
129
|
-
parsing, the TUI selection word model in `vendor/ink`, and interactive turn
|
|
130
|
-
semantics such as queued-message recall, message-selector rewind and
|
|
131
|
-
background-task lifetime. The Anthropic OAuth route additionally sends the
|
|
132
|
-
client identity that Anthropic's token edge validates; those values are a
|
|
133
|
-
wire requirement, not a derivation.
|
|
134
|
-
- **opencode-antigravity-auth** — MIT
|
|
135
|
-
(<https://github.com/NoeFabris/opencode-antigravity-auth>). The Google Cloud
|
|
136
|
-
Code Assist wire contract used by the Antigravity OAuth provider: OAuth
|
|
137
|
-
client parameters, endpoint fallback order, client impersonation headers,
|
|
138
|
-
request envelope and thinking-signature handling.
|
|
139
|
-
- **assistant-ui, Chatbox, Cherry Studio, Jan, Zed** — the heading-size and
|
|
140
|
-
list-density survey behind the desktop markdown ladder in
|
|
141
|
-
`apps/desktop/src/renderer/desktop/22-markdown.css`.
|
package/package.json
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Version-neutral digest for release cache keys.
|
|
5
|
+
*
|
|
6
|
+
* Deploy rewrites the version field of package.json / package-lock.json on
|
|
7
|
+
* every release, so a cache key built from hashFiles() over those manifests is
|
|
8
|
+
* guaranteed to miss on the one run that needs it: the desktop bundle and the
|
|
9
|
+
* five prepared platform runtimes were rebuilt from scratch each release even
|
|
10
|
+
* though their inputs were byte-identical apart from the version string.
|
|
11
|
+
* Hashing the same manifests with the package identity stripped keeps the key
|
|
12
|
+
* pinned to the dependency tree that actually decides the output.
|
|
13
|
+
*/
|
|
14
|
+
import { createHash } from 'node:crypto'
|
|
15
|
+
import { readFile } from 'node:fs/promises'
|
|
16
|
+
import { resolve } from 'node:path'
|
|
17
|
+
import { pathToFileURL } from 'node:url'
|
|
18
|
+
|
|
19
|
+
import { normalizeRuntimeLockfile } from './runtime-dependency-cache-key.mjs'
|
|
20
|
+
|
|
21
|
+
export const MANIFEST_CACHE_SCHEMA = 1
|
|
22
|
+
|
|
23
|
+
export function manifestCacheKey(manifests) {
|
|
24
|
+
const digest = createHash('sha256')
|
|
25
|
+
digest.update(`manifest-v${MANIFEST_CACHE_SCHEMA}`)
|
|
26
|
+
for (const manifest of manifests) {
|
|
27
|
+
digest.update(JSON.stringify(normalizeRuntimeLockfile(manifest)))
|
|
28
|
+
}
|
|
29
|
+
return digest.digest('hex')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const invokedPath = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : ''
|
|
33
|
+
if (invokedPath === import.meta.url) {
|
|
34
|
+
const manifestPaths = process.argv.slice(2)
|
|
35
|
+
if (!manifestPaths.length) {
|
|
36
|
+
throw new Error('Usage: manifest-cache-key.mjs <package.json|package-lock.json>...')
|
|
37
|
+
}
|
|
38
|
+
Promise.all(manifestPaths.map((path) => readFile(resolve(path))))
|
|
39
|
+
.then((manifests) => process.stdout.write(`${manifestCacheKey(manifests)}\n`))
|
|
40
|
+
.catch((error) => {
|
|
41
|
+
process.stderr.write(`Manifest cache key failed: ${error?.message || error}\n`)
|
|
42
|
+
process.exitCode = 1
|
|
43
|
+
})
|
|
44
|
+
}
|
|
@@ -110,7 +110,7 @@ export function resolveGrokOAuthResponsesTransport() {
|
|
|
110
110
|
// Retired model aliases xAI no longer exposes by their old ids. The live
|
|
111
111
|
// catalog surfaces the coding model as grok-build-0.1; map the legacy ids to
|
|
112
112
|
// it so a stale config selection doesn't hit a model-not-found. Exact table,
|
|
113
|
-
// not a heuristic.
|
|
113
|
+
// not a heuristic.
|
|
114
114
|
const RETIRED_MODEL_ALIASES = Object.freeze({
|
|
115
115
|
'grok-code-fast-1': 'grok-build-0.1',
|
|
116
116
|
'grok-code-fast': 'grok-build-0.1',
|
|
@@ -215,10 +215,10 @@ export function isNonTerminalStreamClose(err) {
|
|
|
215
215
|
/**
|
|
216
216
|
* Should this failed stream be re-issued as a NON-STREAMING request?
|
|
217
217
|
*
|
|
218
|
-
*
|
|
219
|
-
* `stream:false` instead of failing the turn (
|
|
218
|
+
* A last safety net: when a stream dies, repeat the same request with
|
|
219
|
+
* `stream:false` instead of failing the turn (gated off only when
|
|
220
220
|
* streaming tool execution could double-run a tool). MixDog dispatches tools
|
|
221
|
-
* eagerly, so this stays deliberately
|
|
221
|
+
* eagerly, so this stays deliberately narrow: only a stream that
|
|
222
222
|
* exposed NOTHING qualifies. An exposed stream is already covered by the
|
|
223
223
|
* loop-level retraction replay (send-with-recovery), which asks the owner to
|
|
224
224
|
* withdraw the rendered characters first.
|
|
@@ -3,9 +3,8 @@ import { TOOL_OUTPUT_MAX_BYTES } from './tool-output-limit.mjs';
|
|
|
3
3
|
|
|
4
4
|
// Read tool caps.
|
|
5
5
|
//
|
|
6
|
-
// READ_MAX_SIZE_BYTES (10 MB) — fast-path file-size threshold
|
|
7
|
-
//
|
|
8
|
-
// this size use readFile + in-memory split by default, which CC measured at
|
|
6
|
+
// READ_MAX_SIZE_BYTES (10 MB) — fast-path file-size threshold. Files at or below
|
|
7
|
+
// this size use readFile + in-memory split by default, measured at
|
|
9
8
|
// ~2x faster than createReadStream + readline for typical source. Explicit
|
|
10
9
|
// offset/limit windows on files above READ_STREAM_RANGE_MIN_BYTES take the
|
|
11
10
|
// streaming path too, so a targeted read avoids materialising a whole
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
// Optional-sharp image resize / downsample helper
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// budget-bounded image block instead of refusing oversized originals.
|
|
1
|
+
// Optional-sharp image resize / downsample helper: a `read` on an image
|
|
2
|
+
// returns a viewable, budget-bounded image block instead of refusing
|
|
3
|
+
// oversized originals.
|
|
5
4
|
//
|
|
6
5
|
// sharp is a direct runtime dependency. Entry points still degrade to `null`
|
|
7
6
|
// when a platform-native binding cannot load so a damaged install reports the
|
|
@@ -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
|
|
@@ -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); }
|
|
@@ -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
|
|
@@ -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) {
|
package/src/tui/dist/index.mjs
CHANGED
|
@@ -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/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) {
|