@north-light/crouter 0.3.176 → 0.3.178
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/dist/api/client.d.ts +4 -4
- package/dist/api/client.js +2 -2
- package/dist/api/dto/lifecycle.d.ts +1 -1
- package/dist/api/dto/reports.d.ts +1 -1
- package/dist/clients/attach/__tests__/frame-selection.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/frame-selection.test.js +84 -0
- package/dist/clients/attach/__tests__/group-activity.test.js +2 -2
- package/dist/clients/attach/ansi-cells.d.ts +31 -0
- package/dist/clients/attach/ansi-cells.js +120 -0
- package/dist/clients/attach/config.js +5 -5
- package/dist/clients/attach/input/clipboard-image.js +2 -2
- package/dist/clients/attach/input/ref-autocomplete.js +1 -1
- package/dist/clients/attach/input/titled-editor.d.ts +12 -13
- package/dist/clients/attach/input/titled-editor.js +14 -15
- package/dist/clients/attach/overlays/dialogs.d.ts +1 -1
- package/dist/clients/attach/overlays/dialogs.js +2 -2
- package/dist/clients/attach/overlays/help.js +2 -1
- package/dist/clients/attach/overlays/mcp.js +5 -12
- package/dist/clients/attach/render/chat-view.d.ts +1 -1
- package/dist/clients/attach/render/chat-view.js +2 -2
- package/dist/clients/attach/render/group-recap.js +7 -0
- package/dist/clients/attach/session/context.js +3 -3
- package/dist/clients/attach/session/editor-inventory.js +6 -6
- package/dist/clients/attach/session/file-links.js +15 -50
- package/dist/clients/attach/session/frame.d.ts +5 -1
- package/dist/clients/attach/session/frame.js +19 -4
- package/dist/clients/attach/session/input-wiring.js +2 -2
- package/dist/clients/attach/session/mouse.d.ts +4 -0
- package/dist/clients/attach/session/mouse.js +14 -3
- package/dist/clients/attach/session/selection.d.ts +28 -0
- package/dist/clients/attach/session/selection.js +108 -0
- package/dist/clients/attach/session/teardown.js +7 -9
- package/dist/clients/attach/slash/dispatch.d.ts +1 -2
- package/dist/clients/attach/slash/dispatch.js +5 -9
- package/dist/clients/attach/viewer.js +552 -550
- package/dist/clients/inbox/controller.js +3 -2
- package/dist/clients/inbox/deck-adapter.js +3 -2
- package/dist/clients/surfaces/host.d.ts +1 -1
- package/dist/commands/api-client.d.ts +12 -13
- package/dist/commands/api-client.js +15 -16
- package/dist/commands/chord.js +1 -2
- package/dist/commands/human/queue.js +6 -5
- package/dist/commands/memory/find.js +3 -3
- package/dist/commands/node/lifecycle.js +1 -1
- package/dist/commands/node-lifecycle-revive.d.ts +1 -1
- package/dist/commands/node-lifecycle-revive.js +3 -3
- package/dist/commands/push.js +1 -1
- package/dist/commands/surface/node/placement.d.ts +1 -1
- package/dist/commands/surface/node/placement.js +1 -1
- package/dist/commands/surface-edit.js +1 -4
- package/dist/commands/sys/panels/panel.d.ts +3 -3
- package/dist/commands/sys/panels/panel.js +3 -3
- package/dist/commands/sys/setup-core.js +1 -7
- package/dist/commands/sys/sync-deps.js +1 -3
- package/dist/commands/sys/sync-project-guidance.js +2 -11
- package/dist/commands/sys/sysprompt.js +1 -3
- package/dist/core/__tests__/fixtures/fake-engine.js +1 -1
- package/dist/core/__tests__/helpers/harness.d.ts +1 -1
- package/dist/core/__tests__/helpers/harness.js +1 -1
- package/dist/core/canvas/boot.js +1 -1
- package/dist/core/canvas/browse/app.js +2 -2
- package/dist/core/canvas/canvas.d.ts +3 -3
- package/dist/core/canvas/canvas.js +3 -3
- package/dist/core/canvas/daemon-owner.js +1 -2
- package/dist/core/canvas/db.js +7 -8
- package/dist/core/canvas/index.js +2 -2
- package/dist/core/canvas/pid.d.ts +12 -14
- package/dist/core/canvas/pid.js +16 -20
- package/dist/core/canvas/remote-transport.js +2 -2
- package/dist/core/canvas/status-glyph.d.ts +1 -1
- package/dist/core/canvas/status-glyph.js +1 -1
- package/dist/core/canvas/types.d.ts +1 -1
- package/dist/core/command-manifests/manifest.js +4 -6
- package/dist/core/command-manifests/schema.js +10 -12
- package/dist/core/command-plugins/transport/exec-invoke.js +5 -7
- package/dist/core/command-plugins/transport/http-invoke.js +4 -6
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.js +2 -2
- package/dist/core/events/read.js +1 -3
- package/dist/core/fault-classifier.js +1 -3
- package/dist/core/fs-utils.d.ts +7 -0
- package/dist/core/fs-utils.js +29 -2
- package/dist/core/host-exports/export.d.ts +4 -5
- package/dist/core/host-exports/export.js +4 -5
- package/dist/core/human/claim.js +3 -2
- package/dist/core/human/convention.d.ts +0 -1
- package/dist/core/human/convention.js +1 -9
- package/dist/core/human/scan.js +5 -4
- package/dist/core/profiles/manifest.d.ts +1 -1
- package/dist/core/profiles/manifest.js +1 -1
- package/dist/core/review/types.d.ts +1 -1
- package/dist/core/review/types.js +1 -1
- package/dist/core/runtime/broker/frame-dispatch.js +4 -4
- package/dist/core/runtime/broker-cli.js +1 -1
- package/dist/core/runtime/broker-protocol.d.ts +9 -10
- package/dist/core/runtime/broker-protocol.js +9 -10
- package/dist/core/runtime/broker-sdk.d.ts +1 -1
- package/dist/core/runtime/broker.js +6 -6
- package/dist/core/runtime/host.js +14 -14
- package/dist/core/runtime/launch.d.ts +1 -1
- package/dist/core/runtime/launch.js +1 -1
- package/dist/core/runtime/lifecycle.js +16 -21
- package/dist/core/runtime/node-read.js +2 -3
- package/dist/core/runtime/package-health.js +1 -8
- package/dist/core/runtime/persona.js +2 -2
- package/dist/core/runtime/pi-vendored.d.ts +4 -2
- package/dist/core/runtime/pi-vendored.js +5 -16
- package/dist/core/runtime/promote.js +1 -1
- package/dist/core/runtime/reopen.js +2 -2
- package/dist/core/runtime/revive-all.d.ts +3 -3
- package/dist/core/runtime/revive-all.js +6 -6
- package/dist/core/runtime/revive.js +1 -1
- package/dist/core/runtime/spawn.d.ts +1 -2
- package/dist/core/runtime/spawn.js +2 -3
- package/dist/core/runtime/structured-output.js +4 -6
- package/dist/core/runtime/tmux-driver.d.ts +2 -2
- package/dist/core/runtime/tmux-driver.js +3 -5
- package/dist/core/runtime/tool-group-summary.js +5 -4
- package/dist/core/runtime/warm-pool.js +1 -1
- package/dist/core/scope.js +2 -9
- package/dist/core/self-update.js +3 -3
- package/dist/core/spawn.d.ts +0 -1
- package/dist/core/spawn.js +1 -4
- package/dist/core/substrate/on-read.js +1 -9
- package/dist/core/wake.js +2 -2
- package/dist/core/worktree.d.ts +3 -3
- package/dist/core/worktree.js +5 -7
- package/dist/daemon/api/bridge.js +1 -1
- package/dist/daemon/api/handlers/broker-ops.js +1 -2
- package/dist/daemon/api/handlers/messages.js +2 -2
- package/dist/daemon/api/handlers/nodes.js +1 -1
- package/dist/daemon/api/handlers/reports.js +1 -1
- package/dist/daemon/api/handlers/validate.d.ts +2 -1
- package/dist/daemon/api/handlers/validate.js +2 -3
- package/dist/daemon/cron-run.js +7 -7
- package/dist/daemon/human/finish.js +3 -2
- package/dist/daemon/manage.d.ts +6 -6
- package/dist/daemon/manage.js +15 -16
- package/dist/pi-extensions/canvas-context-intro.js +1 -14
- package/dist/pi-extensions/canvas-doc-substrate.js +1 -1
- package/dist/pi-extensions/canvas-review-boundary.js +1 -14
- package/dist/pi-extensions/truncate.d.ts +5 -0
- package/dist/pi-extensions/truncate.js +14 -0
- package/dist/shared/predicates.d.ts +2 -0
- package/dist/shared/predicates.js +4 -0
- package/dist/shared/shell-quote.d.ts +3 -0
- package/dist/shared/shell-quote.js +5 -0
- package/dist/shared/tool-groups.d.ts +4 -2
- package/dist/shared/tool-groups.js +3 -3
- package/dist/types.d.ts +4 -5
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
package/dist/api/client.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export interface CrtrClientOptions {
|
|
|
33
33
|
* autostart is on; after it resolves, the client polls `/healthz` and retries
|
|
34
34
|
* the original request once. Absent → a cold socket throws `daemon_unavailable`. */
|
|
35
35
|
onColdSocket?: () => Promise<void>;
|
|
36
|
-
/** Injected cold-start diagnostic
|
|
36
|
+
/** Injected cold-start diagnostic. Called ONLY when the bounded
|
|
37
37
|
* `/healthz` poll times out after `onColdSocket`, so the caller can attach
|
|
38
38
|
* operator-useful context (e.g. a bounded tail of crtrd's stderr log) to the
|
|
39
39
|
* `daemon_unavailable` error instead of it staying a bare message. Must
|
|
@@ -43,7 +43,7 @@ export interface CrtrClientOptions {
|
|
|
43
43
|
/** Bounded window (ms) to poll `/healthz` after `onColdSocket` resolves
|
|
44
44
|
* before giving up with `daemon_unavailable`. Defaults to
|
|
45
45
|
* `HEALTHZ_POLL_WINDOW_MS`. This must NOT silently drift from whatever
|
|
46
|
-
* window actually governs "did the daemon start"
|
|
46
|
+
* window actually governs "did the daemon start":
|
|
47
47
|
* `onColdSocket` is fire-and-forget, so this poll is the ONLY deadline that
|
|
48
48
|
* determines whether the CLI reports success. A caller whose `onColdSocket`
|
|
49
49
|
* hook triggers a differently-windowed startup verifier (e.g. the CLI's
|
|
@@ -253,8 +253,8 @@ export declare class CrtrClient {
|
|
|
253
253
|
private handleColdSocket;
|
|
254
254
|
}
|
|
255
255
|
/** Compose the cold-start `/healthz`-timeout `daemon_unavailable` message,
|
|
256
|
-
* appending the injected diagnostic
|
|
257
|
-
*
|
|
256
|
+
* appending the injected diagnostic when one is present instead of discarding
|
|
257
|
+
* the real startup failure behind a bare message. Exported (not
|
|
258
258
|
* from the package's public `index.ts` surface, which is dependency-light by
|
|
259
259
|
* design) purely so the regression test can assert the composition without
|
|
260
260
|
* waiting out the real poll window. */
|
package/dist/api/client.js
CHANGED
|
@@ -655,8 +655,8 @@ function sleep(ms) {
|
|
|
655
655
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
656
656
|
}
|
|
657
657
|
/** Compose the cold-start `/healthz`-timeout `daemon_unavailable` message,
|
|
658
|
-
* appending the injected diagnostic
|
|
659
|
-
*
|
|
658
|
+
* appending the injected diagnostic when one is present instead of discarding
|
|
659
|
+
* the real startup failure behind a bare message. Exported (not
|
|
660
660
|
* from the package's public `index.ts` surface, which is dependency-light by
|
|
661
661
|
* design) purely so the regression test can assert the composition without
|
|
662
662
|
* waiting out the real poll window. */
|
|
@@ -3,7 +3,7 @@ import type { NodeIdDTO, NodeStatusDTO } from './common.js';
|
|
|
3
3
|
export interface ReviveRequest {
|
|
4
4
|
/** Resume the saved conversation (true) vs a fresh launch (false). */
|
|
5
5
|
resume?: boolean;
|
|
6
|
-
/** Clear the finalization latch before reviving (
|
|
6
|
+
/** Clear the finalization latch before reviving (the `--reopen` gate). The
|
|
7
7
|
* latch clear is a canvas write, so the gate is enforced server-side: with
|
|
8
8
|
* `reopen:false` (default) a finalized node is REJECTED (`node_finalized`);
|
|
9
9
|
* with `reopen:true` a node that is NOT finalized is rejected
|
|
@@ -20,7 +20,7 @@ export interface PushReportResultDTO {
|
|
|
20
20
|
/** Present (and true) only for a `final` push whose reporting node owned a
|
|
21
21
|
* managed worktree with zero commits ahead of its base and no uncommitted
|
|
22
22
|
* changes: the server auto-dropped it instead of blocking on `node worktree
|
|
23
|
-
* close
|
|
23
|
+
* close`. A worktree with real unlanded work instead blocks with
|
|
24
24
|
* an `open_managed_worktree` error. */
|
|
25
25
|
worktree_auto_dropped?: boolean;
|
|
26
26
|
/** Present only alongside `worktree_auto_dropped`: the checkout path dropped. */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Mouse drag-select copies out of the PHYSICAL frame rows, which carry SGR
|
|
2
|
+
// color, OSC-8 link boundaries, pi-tui's APC cursor marker, and wide glyphs. A
|
|
3
|
+
// column is a terminal cell, never a string index — get that wrong and the copy
|
|
4
|
+
// silently slices mid-escape or off-by-N, with the highlight still looking right
|
|
5
|
+
// on screen.
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import test from 'node:test';
|
|
8
|
+
import { highlightCells, rowCells, sliceCells } from '../ansi-cells.js';
|
|
9
|
+
import { createFrameSelection } from '../session/selection.js';
|
|
10
|
+
const CURSOR_MARKER = '\x1b_pi-cursor\x1b\\';
|
|
11
|
+
const LINK = (uri, text) => `\x1b]8;;${uri}\x07${text}\x1b]8;;\x07`;
|
|
12
|
+
test('cells are counted as the terminal counts them', () => {
|
|
13
|
+
assert.equal(rowCells('plain'), 5);
|
|
14
|
+
assert.equal(rowCells('\x1b[31mred\x1b[0m'), 3);
|
|
15
|
+
assert.equal(rowCells(LINK('file:///tmp/a.ts', 'a.ts')), 4);
|
|
16
|
+
assert.equal(rowCells(`ab${CURSOR_MARKER}cd`), 4);
|
|
17
|
+
assert.equal(rowCells('日本'), 4);
|
|
18
|
+
});
|
|
19
|
+
test('a slice returns the plain text under those cells', () => {
|
|
20
|
+
assert.equal(sliceCells('\x1b[31mhello\x1b[0m world', 0, 5), 'hello');
|
|
21
|
+
assert.equal(sliceCells('\x1b[31mhello\x1b[0m world', 6, 11), 'world');
|
|
22
|
+
assert.equal(sliceCells(`src/${LINK('file:///tmp/a.ts', 'a.ts')}:12`, 0, 8), 'src/a.ts');
|
|
23
|
+
assert.equal(sliceCells(`ab${CURSOR_MARKER}cd`, 0, 4), 'abcd');
|
|
24
|
+
// A wide glyph belongs to the cell it starts at and is never split in half.
|
|
25
|
+
assert.equal(sliceCells('日本語', 2, 4), '本');
|
|
26
|
+
});
|
|
27
|
+
test('highlighting restyles without changing what the row says', () => {
|
|
28
|
+
const row = '\x1b[31mhello\x1b[0m world';
|
|
29
|
+
const painted = highlightCells(row, 0, 5, 80);
|
|
30
|
+
assert.equal(sliceCells(painted, 0, rowCells(row)), 'hello world');
|
|
31
|
+
assert.equal(rowCells(painted), rowCells(row));
|
|
32
|
+
assert.ok(painted.includes('\x1b[7m'));
|
|
33
|
+
// A reset inside the span would drop the highlight, so inverse is re-asserted.
|
|
34
|
+
assert.equal(highlightCells(row, 0, 11, 80).split('\x1b[7m').length - 1, 2);
|
|
35
|
+
});
|
|
36
|
+
test('the cursor marker survives a highlight that starts on its cell', () => {
|
|
37
|
+
const painted = highlightCells(`ab${CURSOR_MARKER}cd`, 2, 4, 80);
|
|
38
|
+
assert.ok(painted.includes(CURSOR_MARKER));
|
|
39
|
+
assert.equal(sliceCells(painted, 0, 4), 'abcd');
|
|
40
|
+
});
|
|
41
|
+
test('a short row pads so a multi-row selection reads as one block, never past the pane', () => {
|
|
42
|
+
assert.equal(rowCells(highlightCells('ab', 0, 6, 80)), 6);
|
|
43
|
+
// Padding past the pane width would wrap the row and break the frame's row count.
|
|
44
|
+
assert.equal(rowCells(highlightCells('ab', 0, 6, 4)), 4);
|
|
45
|
+
});
|
|
46
|
+
test('a press that never moves stays a click, so file links still open', () => {
|
|
47
|
+
const selection = createFrameSelection({ frame: () => ['src/a.ts'], width: () => 40 });
|
|
48
|
+
selection.begin(3, 1);
|
|
49
|
+
assert.deepEqual(selection.release(), { kind: 'click', column: 3, row: 1 });
|
|
50
|
+
assert.equal(selection.active(), false);
|
|
51
|
+
});
|
|
52
|
+
test('a drag across rows selects terminal-style: to the edge, then whole rows', () => {
|
|
53
|
+
const frame = ['first row', '\x1b[32msecond row\x1b[0m', 'third row'];
|
|
54
|
+
const selection = createFrameSelection({ frame: () => frame, width: () => 40 });
|
|
55
|
+
selection.begin(7, 1);
|
|
56
|
+
assert.equal(selection.extend(5, 3), true);
|
|
57
|
+
// Same cell twice is not a change — no wasted render.
|
|
58
|
+
assert.equal(selection.extend(5, 3), false);
|
|
59
|
+
const painted = selection.apply(frame);
|
|
60
|
+
assert.equal(painted.length, 3);
|
|
61
|
+
assert.ok(painted[0].includes('\x1b[7m'));
|
|
62
|
+
assert.equal(sliceCells(painted[1], 0, rowCells(frame[1])), 'second row');
|
|
63
|
+
assert.equal(selection.active(), true);
|
|
64
|
+
assert.equal(selection.clear(), true);
|
|
65
|
+
assert.equal(selection.active(), false);
|
|
66
|
+
});
|
|
67
|
+
test('a backwards drag selects the same span as a forwards one', () => {
|
|
68
|
+
const frame = ['abcdef', 'ghijkl'];
|
|
69
|
+
const forwards = createFrameSelection({ frame: () => frame, width: () => 6 });
|
|
70
|
+
forwards.begin(3, 1);
|
|
71
|
+
forwards.extend(4, 2);
|
|
72
|
+
const backwards = createFrameSelection({ frame: () => frame, width: () => 6 });
|
|
73
|
+
backwards.begin(4, 2);
|
|
74
|
+
backwards.extend(3, 1);
|
|
75
|
+
assert.deepEqual(backwards.apply(frame), forwards.apply(frame));
|
|
76
|
+
});
|
|
77
|
+
test('an empty drag over blank rows reports nothing to copy', () => {
|
|
78
|
+
// Releasing over content would shell out to the clipboard; blank rows settle
|
|
79
|
+
// the geometry without leaving the process.
|
|
80
|
+
const selection = createFrameSelection({ frame: () => ['', '', ''], width: () => 40 });
|
|
81
|
+
selection.begin(2, 1);
|
|
82
|
+
selection.extend(5, 3);
|
|
83
|
+
assert.deepEqual(selection.release(), { kind: 'selection', notice: 'Nothing to copy — selection is empty' });
|
|
84
|
+
});
|
|
@@ -30,7 +30,7 @@ async function summarizedGroup(messages, key) {
|
|
|
30
30
|
stats: {},
|
|
31
31
|
state: { isStreaming: false },
|
|
32
32
|
toolGroupSummaries: {
|
|
33
|
-
[key]: { bullets: ['broker recap'], nodesSpawned: 0,
|
|
33
|
+
[key]: { bullets: ['broker recap'], nodesSpawned: 0, filesDeleted: 0 },
|
|
34
34
|
},
|
|
35
35
|
});
|
|
36
36
|
const group = internals.groups.find((candidate) => candidate.key === key);
|
|
@@ -70,7 +70,7 @@ test('a folded recap carries paths and counts, never diff or write payload bodie
|
|
|
70
70
|
bashMode: tone('bashMode'), bashModeAlt: tone('bashModeAlt'),
|
|
71
71
|
};
|
|
72
72
|
const recap = new GroupRecapComponent();
|
|
73
|
-
recap.setRecap({ bullets: ['finished'], nodesSpawned: 0,
|
|
73
|
+
recap.setRecap({ bullets: ['finished'], nodesSpawned: 0, filesDeleted: 2 }, 2, { memories: [{ action: 'read', name: 'alpha', path: '/tmp/memories/alpha.md' }], files: recorder.activity().files }, true, palette);
|
|
74
74
|
const text = recap.render(120).join('\n');
|
|
75
75
|
assert.match(text, /overwritten\.ts/, 'the recap still names what changed');
|
|
76
76
|
assert.doesNotMatch(text, /sensitive-old-diff|sensitive-new-diff|sensitive-write-payload/, 'payload bodies never leak into a recap');
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Read one terminal control sequence so non-printing ANSI never takes a cell.
|
|
2
|
+
* `undefined` means the sequence runs off the end of the row. */
|
|
3
|
+
export declare function readEscape(line: string, start: number): {
|
|
4
|
+
text: string;
|
|
5
|
+
end: number;
|
|
6
|
+
} | undefined;
|
|
7
|
+
/** One printable grapheme, with the escape sequences that preceded it. */
|
|
8
|
+
export type RowGlyph = {
|
|
9
|
+
/** The grapheme itself, with no ANSI attached. */
|
|
10
|
+
text: string;
|
|
11
|
+
/** 0-indexed terminal cell this grapheme starts at. */
|
|
12
|
+
cell: number;
|
|
13
|
+
/** Cells occupied — 2 for a wide grapheme, 0 for a zero-width one. */
|
|
14
|
+
width: number;
|
|
15
|
+
};
|
|
16
|
+
/** Walk `line` as the terminal does, calling `onGlyph` per printable grapheme
|
|
17
|
+
* and `onEscape` per control sequence, in row order. Returns the cell count. */
|
|
18
|
+
export declare function walkRow(line: string, visit: {
|
|
19
|
+
onGlyph?: (glyph: RowGlyph) => void;
|
|
20
|
+
onEscape?: (escape: string) => void;
|
|
21
|
+
}): number;
|
|
22
|
+
/** Cells the row occupies on screen. */
|
|
23
|
+
export declare function rowCells(line: string): number;
|
|
24
|
+
/** Plain text of cells `[from, to)` — every escape, link boundary, and cursor
|
|
25
|
+
* marker dropped, exactly what a terminal copies out of that span. */
|
|
26
|
+
export declare function sliceCells(line: string, from: number, to: number): string;
|
|
27
|
+
/** Reverse-video cells `[from, to)`, re-asserting inverse after any SGR inside
|
|
28
|
+
* the span (a reset there would otherwise drop the highlight mid-selection).
|
|
29
|
+
* A short row is padded with spaces so a multi-row selection reads as one
|
|
30
|
+
* block, never past `maxCells`. */
|
|
31
|
+
export declare function highlightCells(line: string, from: number, to: number, maxCells: number): string;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// ansi-cells.ts — cell arithmetic over a rendered terminal row.
|
|
2
|
+
//
|
|
3
|
+
// A composed frame row mixes printable graphemes with SGR colors, OSC-8 link
|
|
4
|
+
// boundaries, and pi-tui's APC cursor marker. Anything that addresses a row by
|
|
5
|
+
// terminal column — link hit tests, mouse selection — has to walk it the way the
|
|
6
|
+
// terminal does: escapes take no cell, wide graphemes take two. Both the attach
|
|
7
|
+
// session and the renderers need that walk, so it lives at the client root
|
|
8
|
+
// rather than inside either peer.
|
|
9
|
+
import stringWidth from 'string-width';
|
|
10
|
+
const GRAPHEMES = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
11
|
+
const INVERSE_ON = '\x1b[7m';
|
|
12
|
+
const INVERSE_OFF = '\x1b[27m';
|
|
13
|
+
/** Read one terminal control sequence so non-printing ANSI never takes a cell.
|
|
14
|
+
* `undefined` means the sequence runs off the end of the row. */
|
|
15
|
+
export function readEscape(line, start) {
|
|
16
|
+
const kind = line[start + 1];
|
|
17
|
+
if (kind === undefined)
|
|
18
|
+
return undefined;
|
|
19
|
+
// OSC, APC, DCS, and PM are string controls terminated by BEL or ST. pi-tui's
|
|
20
|
+
// cursor marker uses APC, so it must consume no display cells here.
|
|
21
|
+
if (kind === ']' || kind === '_' || kind === 'P' || kind === '^') {
|
|
22
|
+
const bel = line.indexOf('\x07', start + 2);
|
|
23
|
+
const st = line.indexOf('\x1b\\', start + 2);
|
|
24
|
+
const end = bel === -1 ? st : st === -1 ? bel : Math.min(bel, st);
|
|
25
|
+
if (end === -1)
|
|
26
|
+
return undefined;
|
|
27
|
+
return { text: line.slice(start, end + (end === bel ? 1 : 2)), end: end + (end === bel ? 1 : 2) };
|
|
28
|
+
}
|
|
29
|
+
if (kind === '[') {
|
|
30
|
+
let end = start + 2;
|
|
31
|
+
while (end < line.length && (line.charCodeAt(end) < 0x40 || line.charCodeAt(end) > 0x7e))
|
|
32
|
+
end++;
|
|
33
|
+
if (end >= line.length)
|
|
34
|
+
return undefined;
|
|
35
|
+
return { text: line.slice(start, end + 1), end: end + 1 };
|
|
36
|
+
}
|
|
37
|
+
return { text: line.slice(start, start + 2), end: start + 2 };
|
|
38
|
+
}
|
|
39
|
+
/** Walk `line` as the terminal does, calling `onGlyph` per printable grapheme
|
|
40
|
+
* and `onEscape` per control sequence, in row order. Returns the cell count. */
|
|
41
|
+
export function walkRow(line, visit) {
|
|
42
|
+
let cell = 0;
|
|
43
|
+
let index = 0;
|
|
44
|
+
while (index < line.length) {
|
|
45
|
+
if (line[index] === '\x1b') {
|
|
46
|
+
const escape = readEscape(line, index);
|
|
47
|
+
if (escape === undefined)
|
|
48
|
+
break;
|
|
49
|
+
visit.onEscape?.(escape.text);
|
|
50
|
+
index = escape.end;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const nextEscape = line.indexOf('\x1b', index);
|
|
54
|
+
const text = line.slice(index, nextEscape === -1 ? undefined : nextEscape);
|
|
55
|
+
for (const segment of GRAPHEMES.segment(text)) {
|
|
56
|
+
const width = stringWidth(segment.segment);
|
|
57
|
+
visit.onGlyph?.({ text: segment.segment, cell, width });
|
|
58
|
+
cell += width;
|
|
59
|
+
}
|
|
60
|
+
index = nextEscape === -1 ? line.length : nextEscape;
|
|
61
|
+
}
|
|
62
|
+
return cell;
|
|
63
|
+
}
|
|
64
|
+
/** Cells the row occupies on screen. */
|
|
65
|
+
export function rowCells(line) {
|
|
66
|
+
return walkRow(line, {});
|
|
67
|
+
}
|
|
68
|
+
/** Plain text of cells `[from, to)` — every escape, link boundary, and cursor
|
|
69
|
+
* marker dropped, exactly what a terminal copies out of that span. */
|
|
70
|
+
export function sliceCells(line, from, to) {
|
|
71
|
+
let text = '';
|
|
72
|
+
walkRow(line, {
|
|
73
|
+
onGlyph: (glyph) => {
|
|
74
|
+
if (glyph.cell >= from && glyph.cell < to)
|
|
75
|
+
text += glyph.text;
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
return text;
|
|
79
|
+
}
|
|
80
|
+
/** Reverse-video cells `[from, to)`, re-asserting inverse after any SGR inside
|
|
81
|
+
* the span (a reset there would otherwise drop the highlight mid-selection).
|
|
82
|
+
* A short row is padded with spaces so a multi-row selection reads as one
|
|
83
|
+
* block, never past `maxCells`. */
|
|
84
|
+
export function highlightCells(line, from, to, maxCells) {
|
|
85
|
+
const end = Math.min(to, maxCells);
|
|
86
|
+
if (end <= from || from < 0)
|
|
87
|
+
return line;
|
|
88
|
+
let out = '';
|
|
89
|
+
let inside = false;
|
|
90
|
+
const open = () => { if (!inside) {
|
|
91
|
+
out += INVERSE_ON;
|
|
92
|
+
inside = true;
|
|
93
|
+
} };
|
|
94
|
+
const close = () => { if (inside) {
|
|
95
|
+
out += INVERSE_OFF;
|
|
96
|
+
inside = false;
|
|
97
|
+
} };
|
|
98
|
+
const cells = walkRow(line, {
|
|
99
|
+
onEscape: (escape) => {
|
|
100
|
+
out += escape;
|
|
101
|
+
// An SGR inside the span may have reset every attribute, including ours.
|
|
102
|
+
if (inside && escape.endsWith('m'))
|
|
103
|
+
out += INVERSE_ON;
|
|
104
|
+
},
|
|
105
|
+
onGlyph: (glyph) => {
|
|
106
|
+
if (glyph.cell >= from && glyph.cell < end)
|
|
107
|
+
open();
|
|
108
|
+
else
|
|
109
|
+
close();
|
|
110
|
+
out += glyph.text;
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
close();
|
|
114
|
+
if (cells < end) {
|
|
115
|
+
const gap = Math.max(0, from - cells);
|
|
116
|
+
const highlighted = end - Math.max(from, cells);
|
|
117
|
+
out += ' '.repeat(gap) + INVERSE_ON + ' '.repeat(highlighted) + INVERSE_OFF;
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
@@ -29,10 +29,10 @@ export function defaultAgentDir() {
|
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
31
|
* pi's `app.*` editor keybindings — VENDORED from pi `core/keybindings.ts`
|
|
32
|
-
* `KEYBINDINGS` (the const is not exported
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
32
|
+
* `KEYBINDINGS` (the const is not exported). This is the subset CustomEditor's
|
|
33
|
+
* main editor consults; the selector-internal bindings (`app.tree.*`,
|
|
34
|
+
* `app.models.*`, session delete/rename) are omitted because the attach viewer
|
|
35
|
+
* does not open those selectors. Re-sync on each SDK
|
|
36
36
|
* bump against `core/keybindings.js`.
|
|
37
37
|
*/
|
|
38
38
|
const APP_KEYBINDINGS = {
|
|
@@ -148,7 +148,7 @@ export async function mirrorKittyProtocolToEditor(active) {
|
|
|
148
148
|
* Resolve the `fd` binary path the @-mention file picker needs. pi-tui's
|
|
149
149
|
* `CombinedAutocompleteProvider` only returns file suggestions when handed an
|
|
150
150
|
* `fdPath` (its third ctor arg) — without it, typing `@` triggers the picker but
|
|
151
|
-
* `getFuzzyFileSuggestions` returns `[]`, so the picker never appears
|
|
151
|
+
* `getFuzzyFileSuggestions` returns `[]`, so the picker never appears.
|
|
152
152
|
* pi's own interactive mode resolves this via `ensureTool('fd')` from its
|
|
153
153
|
* internal `utils/tools-manager` (checks pi's managed bin dir + system PATH,
|
|
154
154
|
* downloading the binary if missing). That module is NOT on pi-coding-agent's
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// input/clipboard-image.ts — read the clipboard image for `crtr surface attach
|
|
1
|
+
// input/clipboard-image.ts — read the clipboard image for `crtr surface attach`.
|
|
2
2
|
//
|
|
3
3
|
// pi's `readClipboardImage` is NOT re-exported (it lives behind a native
|
|
4
4
|
// clipboard binding + Photon worker the package gates off `.`-only exports), so
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// JPEG and picks the smaller, so it doubles as format normalization;
|
|
22
22
|
// `convertToPng` is the fallback when the Photon resizer is unavailable. The
|
|
23
23
|
// fallbacks (convertToPng / raw bytes) are NOT size-bounded by `resizeImage`, so
|
|
24
|
-
// each is gated on the SAME MAX_BYTES base64 ceiling
|
|
24
|
+
// each is gated on the SAME MAX_BYTES base64 ceiling: over it → the
|
|
25
25
|
// image is DROPPED with a user-visible note rather than shipped as an over-cap
|
|
26
26
|
// frame that would overflow BROKER_READ_CAPS.maxLineBytes (24 MiB) and destroy
|
|
27
27
|
// the viewer socket. The result is an `ImageContent` (pi-ai) ready to drop
|
|
@@ -94,7 +94,7 @@ export class RefAwareAutocompleteProvider {
|
|
|
94
94
|
}));
|
|
95
95
|
// Filtering above used the full slash-form `prefix` (context.prefix always
|
|
96
96
|
// starts with '/' for a matched token), but the RETURNED prefix strips that
|
|
97
|
-
// leading '/'
|
|
97
|
+
// leading '/'. pi-tui's `handleInput` (tui.select.confirm
|
|
98
98
|
// branch) falls through to `submitValue()` whenever `autocompletePrefix`
|
|
99
99
|
// (set verbatim from this return value) starts with '/' — that's its own
|
|
100
100
|
// leading-command-completion submit shortcut, and a non-leading ref
|
|
@@ -91,7 +91,7 @@ export declare class TitledEditor extends CustomEditor {
|
|
|
91
91
|
* The base `Editor` resolves newLine against `getKeybindings()` on ITS pi-tui
|
|
92
92
|
* instance, which in a non-deduped install is a DIFFERENT module than the one
|
|
93
93
|
* carrying crtr's `alt+enter` override — so the override can silently never
|
|
94
|
-
* reach it and Alt+Enter falls through to submit
|
|
94
|
+
* reach it and Alt+Enter falls through to submit. crtr's `km` (the
|
|
95
95
|
* same manager CustomEditor matches `app.*` against) always carries the
|
|
96
96
|
* override, so matching the chord here makes the newline behavior independent
|
|
97
97
|
* of cross-instance keybinding mirroring. Skipped while the autocomplete popup
|
|
@@ -159,8 +159,8 @@ export declare class TitledEditor extends CustomEditor {
|
|
|
159
159
|
paneFocused: boolean;
|
|
160
160
|
/** Canonical names the broker currently resolves — the exact-membership set
|
|
161
161
|
* a candidate ref token must be in to render as a resolved highlight.
|
|
162
|
-
* Empty by default (no decoration) until the caller
|
|
163
|
-
*
|
|
162
|
+
* Empty by default (no decoration) until the caller supplies the
|
|
163
|
+
* broker-supplied inventory. */
|
|
164
164
|
private resolvedRefNames;
|
|
165
165
|
/** Command names the VIEWER dispatches ENTIRELY locally (never reach the
|
|
166
166
|
* engine as prose) — checked against the TRIMMED leading-name rule
|
|
@@ -169,15 +169,14 @@ export declare class TitledEditor extends CustomEditor {
|
|
|
169
169
|
* `locallyConsumedCommandNames()` from `slash/dispatch.ts` — independent
|
|
170
170
|
* of `remote` and of the broker's `get_commands` reply (see that
|
|
171
171
|
* function's doc comment). Deliberately NEVER the merged display/
|
|
172
|
-
* completion list (
|
|
173
|
-
*
|
|
172
|
+
* completion list (`slashCommandList` is a different, remote-filtered,
|
|
173
|
+
* broker-augmented list, not a dispatch predicate). */
|
|
174
174
|
private localCommandNames;
|
|
175
175
|
/** Leading tokens the ENGINE (broker) independently consumes as a leading
|
|
176
176
|
* command/skill/template dispatch — the broker's own
|
|
177
|
-
* `engineLeadingCommandNames
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
* `isLeadingEngineCommand` — review Minor 10). Populated from each
|
|
177
|
+
* `engineLeadingCommandNames`, checked against the UNTRIMMED leading-name
|
|
178
|
+
* rule (`matchesEngineLeadingCommand`, mirroring the broker's own
|
|
179
|
+
* `isLeadingEngineCommand`). Populated from each
|
|
181
180
|
* `list_memory_refs` reply, alongside `resolvedRefNames`. */
|
|
182
181
|
private engineCommandNames;
|
|
183
182
|
setResolvedRefNames(names: Iterable<string>): void;
|
|
@@ -192,12 +191,12 @@ export declare class TitledEditor extends CustomEditor {
|
|
|
192
191
|
* is simply left undecorated. Suppressed entirely (command-wide) whenever
|
|
193
192
|
* the composer's own leading token dispatches — as a LOCAL viewer command
|
|
194
193
|
* (`localCommandNames`, TRIMMED rule) or as an ENGINE-consumed leading
|
|
195
|
-
* command/skill/template (`engineCommandNames`, UNTRIMMED rule
|
|
196
|
-
*
|
|
194
|
+
* command/skill/template (`engineCommandNames`, UNTRIMMED rule) — the ref
|
|
195
|
+
* grammar and slash-dispatch's name charset differ, so
|
|
197
196
|
* highlighting a ref inside a command that's about to dispatch would be
|
|
198
197
|
* misleading chrome for text the command, not memory resolution, is about
|
|
199
|
-
* to consume. The two rules are kept deliberately SEPARATE
|
|
200
|
-
*
|
|
198
|
+
* to consume. The two rules are kept deliberately SEPARATE rather than
|
|
199
|
+
* collapsed into one set/predicate: they classify against
|
|
201
200
|
* different text (trimmed vs. raw) and different authorities (this
|
|
202
201
|
* viewer's own dispatch vs. the broker's). */
|
|
203
202
|
private decorateResolvedRefs;
|
|
@@ -401,7 +401,7 @@ function extractLeadingCommandName(text) {
|
|
|
401
401
|
return m ? m[1] : null;
|
|
402
402
|
}
|
|
403
403
|
/** Mirrors the broker's `isLeadingEngineCommand` (`core/runtime/broker.ts`)
|
|
404
|
-
* leading-token rule EXACTLY for classification purposes
|
|
404
|
+
* leading-token rule EXACTLY for classification purposes:
|
|
405
405
|
* the RAW, un-trimmed `text` must itself start with `/` — no `.trim()` — for
|
|
406
406
|
* ANY token to be a candidate, matching the broker's own untrimmed
|
|
407
407
|
* `text.startsWith('/')` checks byte-for-byte (space-based name split,
|
|
@@ -447,7 +447,7 @@ export class TitledEditor extends CustomEditor {
|
|
|
447
447
|
constructor(tui, theme, keybindings, options) {
|
|
448
448
|
super(tui, theme, keybindings, options);
|
|
449
449
|
this.km = keybindings;
|
|
450
|
-
// Preserve raw (untrimmed) submit text end-to-end
|
|
450
|
+
// Preserve raw (untrimmed) submit text end-to-end. The base
|
|
451
451
|
// `Editor.submitValue()` — private in the `.d.ts` (blocking a normal TS
|
|
452
452
|
// `override`), hence the structural cast, same pattern as the other private-
|
|
453
453
|
// internals casts throughout this file (`scrollOffset`, `historyIndex`,
|
|
@@ -497,7 +497,7 @@ export class TitledEditor extends CustomEditor {
|
|
|
497
497
|
* The base `Editor` resolves newLine against `getKeybindings()` on ITS pi-tui
|
|
498
498
|
* instance, which in a non-deduped install is a DIFFERENT module than the one
|
|
499
499
|
* carrying crtr's `alt+enter` override — so the override can silently never
|
|
500
|
-
* reach it and Alt+Enter falls through to submit
|
|
500
|
+
* reach it and Alt+Enter falls through to submit. crtr's `km` (the
|
|
501
501
|
* same manager CustomEditor matches `app.*` against) always carries the
|
|
502
502
|
* override, so matching the chord here makes the newline behavior independent
|
|
503
503
|
* of cross-instance keybinding mirroring. Skipped while the autocomplete popup
|
|
@@ -655,8 +655,8 @@ export class TitledEditor extends CustomEditor {
|
|
|
655
655
|
paneFocused = true;
|
|
656
656
|
/** Canonical names the broker currently resolves — the exact-membership set
|
|
657
657
|
* a candidate ref token must be in to render as a resolved highlight.
|
|
658
|
-
* Empty by default (no decoration) until the caller
|
|
659
|
-
*
|
|
658
|
+
* Empty by default (no decoration) until the caller supplies the
|
|
659
|
+
* broker-supplied inventory. */
|
|
660
660
|
resolvedRefNames = new Set();
|
|
661
661
|
/** Command names the VIEWER dispatches ENTIRELY locally (never reach the
|
|
662
662
|
* engine as prose) — checked against the TRIMMED leading-name rule
|
|
@@ -665,15 +665,14 @@ export class TitledEditor extends CustomEditor {
|
|
|
665
665
|
* `locallyConsumedCommandNames()` from `slash/dispatch.ts` — independent
|
|
666
666
|
* of `remote` and of the broker's `get_commands` reply (see that
|
|
667
667
|
* function's doc comment). Deliberately NEVER the merged display/
|
|
668
|
-
* completion list (
|
|
669
|
-
*
|
|
668
|
+
* completion list (`slashCommandList` is a different, remote-filtered,
|
|
669
|
+
* broker-augmented list, not a dispatch predicate). */
|
|
670
670
|
localCommandNames = new Set();
|
|
671
671
|
/** Leading tokens the ENGINE (broker) independently consumes as a leading
|
|
672
672
|
* command/skill/template dispatch — the broker's own
|
|
673
|
-
* `engineLeadingCommandNames
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
* `isLeadingEngineCommand` — review Minor 10). Populated from each
|
|
673
|
+
* `engineLeadingCommandNames`, checked against the UNTRIMMED leading-name
|
|
674
|
+
* rule (`matchesEngineLeadingCommand`, mirroring the broker's own
|
|
675
|
+
* `isLeadingEngineCommand`). Populated from each
|
|
677
676
|
* `list_memory_refs` reply, alongside `resolvedRefNames`. */
|
|
678
677
|
engineCommandNames = new Set();
|
|
679
678
|
setResolvedRefNames(names) {
|
|
@@ -694,12 +693,12 @@ export class TitledEditor extends CustomEditor {
|
|
|
694
693
|
* is simply left undecorated. Suppressed entirely (command-wide) whenever
|
|
695
694
|
* the composer's own leading token dispatches — as a LOCAL viewer command
|
|
696
695
|
* (`localCommandNames`, TRIMMED rule) or as an ENGINE-consumed leading
|
|
697
|
-
* command/skill/template (`engineCommandNames`, UNTRIMMED rule
|
|
698
|
-
*
|
|
696
|
+
* command/skill/template (`engineCommandNames`, UNTRIMMED rule) — the ref
|
|
697
|
+
* grammar and slash-dispatch's name charset differ, so
|
|
699
698
|
* highlighting a ref inside a command that's about to dispatch would be
|
|
700
699
|
* misleading chrome for text the command, not memory resolution, is about
|
|
701
|
-
* to consume. The two rules are kept deliberately SEPARATE
|
|
702
|
-
*
|
|
700
|
+
* to consume. The two rules are kept deliberately SEPARATE rather than
|
|
701
|
+
* collapsed into one set/predicate: they classify against
|
|
703
702
|
* different text (trimmed vs. raw) and different authorities (this
|
|
704
703
|
* viewer's own dispatch vs. the broker's). */
|
|
705
704
|
decorateResolvedRefs(lines, width) {
|
|
@@ -19,7 +19,7 @@ export declare function isBlockingDialog(req: RpcExtensionUIRequest): boolean;
|
|
|
19
19
|
* `keybindings.matches(key, "app.editor.external")` for the Ctrl+G external-editor
|
|
20
20
|
* shortcut. Defaults to a fresh `KeybindingsManager(TUI_KEYBINDINGS)` — that
|
|
21
21
|
* knows the base TUI bindings but not pi's app-level `app.editor.external`, so
|
|
22
|
-
* Ctrl+G is inert until the input controller
|
|
22
|
+
* Ctrl+G is inert until the input controller passes the app keybindings
|
|
23
23
|
* manager. `ExtensionEditorComponent` is typed against pi's CORE
|
|
24
24
|
* KeybindingsManager (a subclass of pi-tui's), so we widen via the constructor's
|
|
25
25
|
* own parameter type rather than importing the core class (type-only export).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// overlays/dialogs.ts — the blocking-dialog renderer for `crtr surface attach
|
|
1
|
+
// overlays/dialogs.ts — the blocking-dialog renderer for `crtr surface attach`.
|
|
2
2
|
//
|
|
3
3
|
// The broker forwards an extension's user-blocking UI request as an
|
|
4
4
|
// `extension_ui_request` frame (RpcExtensionUIRequest). The viewer renders it
|
|
@@ -45,7 +45,7 @@ const OVERLAY_OPTIONS = { anchor: 'center', width: '70%', maxHeight: '80%' };
|
|
|
45
45
|
* `keybindings.matches(key, "app.editor.external")` for the Ctrl+G external-editor
|
|
46
46
|
* shortcut. Defaults to a fresh `KeybindingsManager(TUI_KEYBINDINGS)` — that
|
|
47
47
|
* knows the base TUI bindings but not pi's app-level `app.editor.external`, so
|
|
48
|
-
* Ctrl+G is inert until the input controller
|
|
48
|
+
* Ctrl+G is inert until the input controller passes the app keybindings
|
|
49
49
|
* manager. `ExtensionEditorComponent` is typed against pi's CORE
|
|
50
50
|
* KeybindingsManager (a subclass of pi-tui's), so we widen via the constructor's
|
|
51
51
|
* own parameter type rather than importing the core class (type-only export).
|
|
@@ -135,6 +135,7 @@ export class HelpOverlay {
|
|
|
135
135
|
muted('mouse wheel (when reporting is on)'),
|
|
136
136
|
blank,
|
|
137
137
|
heading('Selection and copy'),
|
|
138
|
+
{ text: 'Drag with the mouse to select; releasing copies the selection to the clipboard (and the tmux buffer). Any key clears the highlight.', style: 'plain' },
|
|
138
139
|
{ text: 'prefix + [ tmux copy-mode selects and copies the visible screenful; it works even while mouse reporting is on.', style: 'plain' },
|
|
139
140
|
{ text: 'Option-drag (iTerm2, Terminal.app) or Shift-drag (Ghostty, Alacritty, xterm) makes a true terminal selection that bypasses mouse reporting.', style: 'plain' },
|
|
140
141
|
{ text: 'Block-copy commands copy transcript content and are not capped at one screenful.', style: 'plain' },
|
|
@@ -146,7 +147,7 @@ export class HelpOverlay {
|
|
|
146
147
|
{ text: "/copy-transcript every copyable block's text, oldest to newest, joined by blank lines.", style: 'plain' },
|
|
147
148
|
blank,
|
|
148
149
|
heading('Mouse'),
|
|
149
|
-
{ text: `/mouse or ${this.binding('crtr.attach.mouse.toggle')} toggle mouse wheel scrolling. Turning it off restores tmux drag-select and copy-mode mouse.`, style: 'plain' },
|
|
150
|
+
{ text: `/mouse or ${this.binding('crtr.attach.mouse.toggle')} toggle mouse reporting — wheel scrolling and viewer drag-select. Turning it off restores tmux drag-select and copy-mode mouse.`, style: 'plain' },
|
|
150
151
|
blank,
|
|
151
152
|
heading('Other attach keys'),
|
|
152
153
|
...otherKeys,
|
|
@@ -40,12 +40,13 @@
|
|
|
40
40
|
// installed adapter (pi-mcp-adapter index.ts: "auth-start" / "auth-complete" /
|
|
41
41
|
// "ui-messages"; `connect: <name>`).
|
|
42
42
|
import { createHash } from 'node:crypto';
|
|
43
|
-
import { existsSync
|
|
43
|
+
import { existsSync } from 'node:fs';
|
|
44
44
|
import { homedir } from 'node:os';
|
|
45
45
|
import { join, resolve } from 'node:path';
|
|
46
46
|
import { piAgentDir } from '../../../core/runtime/package-health.js';
|
|
47
47
|
import { SelectList, visibleWidth } from '@earendil-works/pi-tui';
|
|
48
48
|
import { getSelectListTheme } from '@earendil-works/pi-coding-agent';
|
|
49
|
+
import { readJsonOrNull } from '../../../core/fs-utils.js';
|
|
49
50
|
/** Adapter metadata-cache.ts CACHE_MAX_AGE_MS — entries older than this are misses. */
|
|
50
51
|
const CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
51
52
|
/** The adapter's import-source table (pi-mcp-adapter config.ts IMPORT_PATHS).
|
|
@@ -72,14 +73,6 @@ function importPathCandidates(kind, home) {
|
|
|
72
73
|
return [];
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
|
-
function readJson(path) {
|
|
76
|
-
try {
|
|
77
|
-
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
return null; // missing or corrupt → empty state, never a crash
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
76
|
/** Mirror of the adapter's validateConfig (config.ts): a source file's own
|
|
84
77
|
* servers may live under `mcpServers` OR the `mcp-servers` alias — but a
|
|
85
78
|
* non-object value invalidates the WHOLE file, dropping its `imports` too. */
|
|
@@ -130,7 +123,7 @@ function mergeServerMaps(base, next) {
|
|
|
130
123
|
/** Read one config file: its own servers layered OVER any `imports` servers
|
|
131
124
|
* (first import wins per name, own config wins overall — adapter expandImports). */
|
|
132
125
|
function readConfigSource(path, cwd, home) {
|
|
133
|
-
const raw =
|
|
126
|
+
const raw = readJsonOrNull(path);
|
|
134
127
|
if (raw === null)
|
|
135
128
|
return {};
|
|
136
129
|
const { servers: own, imports } = validateConfigSource(raw);
|
|
@@ -144,7 +137,7 @@ function readConfigSource(path, cwd, home) {
|
|
|
144
137
|
.find((candidate) => existsSync(candidate));
|
|
145
138
|
if (full === undefined)
|
|
146
139
|
continue;
|
|
147
|
-
const importedRaw =
|
|
140
|
+
const importedRaw = readJsonOrNull(full);
|
|
148
141
|
if (importedRaw === null)
|
|
149
142
|
continue;
|
|
150
143
|
for (const [name, spec] of Object.entries(extractImportedServers(importedRaw, kind))) {
|
|
@@ -228,7 +221,7 @@ export function readMcpServers(home = homedir(), cwd = process.cwd()) {
|
|
|
228
221
|
for (const path of sourcePaths) {
|
|
229
222
|
servers = mergeServerMaps(servers, readConfigSource(path, cwd, home));
|
|
230
223
|
}
|
|
231
|
-
const cache =
|
|
224
|
+
const cache = readJsonOrNull(join(agentDir, 'mcp-cache.json'));
|
|
232
225
|
return Object.entries(servers).map(([name, spec]) => {
|
|
233
226
|
const transport = spec.type ?? (spec.url !== undefined ? 'http' : 'stdio');
|
|
234
227
|
const target = spec.url ?? [spec.command, ...(spec.args ?? [])].filter(Boolean).join(' ');
|
|
@@ -35,7 +35,7 @@ export interface ChatViewOptions {
|
|
|
35
35
|
toolOutputExpanded?: boolean;
|
|
36
36
|
/** Sink for footer/header-only events ChatView does not itself render
|
|
37
37
|
* (queue_update, session_info_changed, thinking_level_changed). The attach
|
|
38
|
-
* command
|
|
38
|
+
* command wires a footer here; absent → these events are no-ops. */
|
|
39
39
|
onFooterEvent?: (event: AgentSessionEvent) => void;
|
|
40
40
|
/** Theme palette for the activity spinner / dim status / error lines. Absent →
|
|
41
41
|
* the module defaults (faint + ANSI red) are used. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// render/chat-view.ts — the render half of `crtr surface attach
|
|
1
|
+
// render/chat-view.ts — the render half of `crtr surface attach`.
|
|
2
2
|
//
|
|
3
3
|
// A direct port of pi's `InteractiveMode.handleEvent()`
|
|
4
4
|
// (interactive-mode.js:2169-2462) + its rebuild path
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// Its activity state (working spinner, compaction/retry indicators) is signaled
|
|
12
12
|
// to the viewer-owned fixed slot when one is supplied. It does NOT own the
|
|
13
13
|
// editor, the footer, or the terminal title — those are chrome laid out by the
|
|
14
|
-
// attach command
|
|
14
|
+
// attach command. Events that only affect that chrome
|
|
15
15
|
// (queue_update / session_info_changed / thinking_level_changed) are forwarded
|
|
16
16
|
// to the optional `onFooterEvent` sink instead of rendered here.
|
|
17
17
|
//
|