@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
|
@@ -6,6 +6,8 @@ import { pathToFileURL } from 'node:url';
|
|
|
6
6
|
import { ICON } from '../../../core/inspector/model.js';
|
|
7
7
|
import { TOOL_ICON } from '../../../core/preview-registry.js';
|
|
8
8
|
const MEMORY_ICON = String.fromCodePoint(0xf02d);
|
|
9
|
+
// trash — deletion has no tool of its own, so it borrows no TOOL_ICON entry.
|
|
10
|
+
const DELETE_ICON = String.fromCodePoint(0xf1f8);
|
|
9
11
|
function count(value, singular, plural) {
|
|
10
12
|
return `${value} ${value === 1 ? singular : plural}`;
|
|
11
13
|
}
|
|
@@ -74,6 +76,11 @@ export class GroupRecapComponent extends Container {
|
|
|
74
76
|
if (changedFiles >= 1) {
|
|
75
77
|
stats.push(`${palette.warning(TOOL_ICON.edit)} ${palette.muted(count(changedFiles, 'file changed', 'files changed'))}`);
|
|
76
78
|
}
|
|
79
|
+
// Deletions never enter activity.files (only edit/write calls do), so the
|
|
80
|
+
// model's count covers a disjoint set and cannot double-count a change.
|
|
81
|
+
if (summary.filesDeleted >= 1) {
|
|
82
|
+
stats.push(`${palette.diffRemoved(DELETE_ICON)} ${palette.muted(count(summary.filesDeleted, 'file deleted', 'files deleted'))}`);
|
|
83
|
+
}
|
|
77
84
|
const memoriesRead = countMemories(activity.memories, 'read');
|
|
78
85
|
if (memoriesRead >= 1) {
|
|
79
86
|
stats.push(`${palette.info(MEMORY_ICON)} ${palette.muted(count(memoriesRead, 'memory read', 'memory reads'))}`);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// session/context.ts — the named home for an attach session's STABLE facts.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// Held as closure locals inside `runAttach`, pulling any block out of it would
|
|
4
|
+
// mean inventing a bespoke parameter list for that block alone.
|
|
5
|
+
// `AttachSession` gives the state one declaration; each
|
|
6
6
|
// collaborator then names the slice it needs as a `Pick<AttachSession, …>`
|
|
7
7
|
// view, so the module's dependencies are a *projection* of one truth rather
|
|
8
8
|
// than a hand-maintained bag that can drift from it.
|
|
@@ -62,8 +62,8 @@ export function createEditorInventory(ctx, capabilities, hooks) {
|
|
|
62
62
|
let brokerCommands;
|
|
63
63
|
let memoryRefs = [];
|
|
64
64
|
// The broker's `engineLeadingCommandNames` from the latest `list_memory_refs`
|
|
65
|
-
// reply
|
|
66
|
-
//
|
|
65
|
+
// reply — leading tokens the ENGINE independently consumes, kept separate
|
|
66
|
+
// from the viewer-local
|
|
67
67
|
// `locallyConsumedCommandNames()` set and fed to the editor's untrimmed
|
|
68
68
|
// engine-suppression rule via `setEngineCommandNames`.
|
|
69
69
|
let engineLeadingCommandNames = [];
|
|
@@ -82,9 +82,9 @@ export function createEditorInventory(ctx, capabilities, hooks) {
|
|
|
82
82
|
// Atomically rebuild the editor's provider/highlight state from whatever
|
|
83
83
|
// command + ref inventory is CURRENTLY known — the one seam `setCommands` and
|
|
84
84
|
// `setMemoryRefs` both call, so a command-only or ref-only response can never
|
|
85
|
-
// rebuild the provider having silently dropped the other inventory
|
|
86
|
-
//
|
|
87
|
-
//
|
|
85
|
+
// rebuild the provider having silently dropped the other inventory. This
|
|
86
|
+
// module owns command metadata, ref metadata, provider rebuild, and editor
|
|
87
|
+
// highlight sets in one place. Command-only
|
|
88
88
|
// autocomplete is unchanged: `RefAwareAutocompleteProvider` fully delegates
|
|
89
89
|
// leading-slash and every non-ref context to the wrapped command provider, so
|
|
90
90
|
// refs never enter command completion or the loaded-command preview (which
|
|
@@ -93,7 +93,7 @@ export function createEditorInventory(ctx, capabilities, hooks) {
|
|
|
93
93
|
const commandProvider = buildAttachAutocompleteProvider(meta.cwd, hooks.fdPath, brokerCommands, capabilities);
|
|
94
94
|
editor.setAutocompleteProvider(new RefAwareAutocompleteProvider(memoryRefs, commandProvider));
|
|
95
95
|
editor.setResolvedRefNames(memoryRefs.map((ref) => ref.name));
|
|
96
|
-
// Two independent suppression inventories
|
|
96
|
+
// Two independent suppression inventories: the viewer-local
|
|
97
97
|
// set (everything `dispatchSlashCommand` handles entirely locally, trimmed
|
|
98
98
|
// rule) and the broker's own `engineLeadingCommandNames` (untrimmed rule) —
|
|
99
99
|
// never the merged display/completion list `slashCommandList` returns.
|
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
// and OSC-8 boundaries have all already been composed there.
|
|
6
6
|
import { existsSync } from 'node:fs';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import
|
|
8
|
+
import { walkRow } from '../ansi-cells.js';
|
|
9
9
|
import { respawnPaneInBackground, shellQuote, viewerSplitEnv } from '../../../core/runtime/placement-tmux.js';
|
|
10
|
-
const GRAPHEMES = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
11
10
|
/** URI of the OSC-8 hyperlink occupying the given 1-indexed terminal cell. */
|
|
12
11
|
export function osc8UriAt(frame, row, column) {
|
|
13
12
|
if (!Number.isInteger(row) || !Number.isInteger(column) || row < 1 || column < 1)
|
|
@@ -24,56 +23,22 @@ export function osc8UriAt(frame, row, column) {
|
|
|
24
23
|
}
|
|
25
24
|
function scanOsc8Row(line, initialUri, hitColumn) {
|
|
26
25
|
let activeUri = initialUri;
|
|
27
|
-
let
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const escape = readEscape(line, index);
|
|
32
|
-
if (escape === undefined)
|
|
33
|
-
break;
|
|
34
|
-
const osc8 = parseOsc8(escape.text);
|
|
26
|
+
let uri;
|
|
27
|
+
walkRow(line, {
|
|
28
|
+
onEscape: (escape) => {
|
|
29
|
+
const osc8 = parseOsc8(escape);
|
|
35
30
|
if (osc8 !== null)
|
|
36
31
|
activeUri = osc8 === '' ? undefined : osc8;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
cell += width;
|
|
48
|
-
}
|
|
49
|
-
index = nextEscape === -1 ? line.length : nextEscape;
|
|
50
|
-
}
|
|
51
|
-
return { activeUri, uri: undefined };
|
|
52
|
-
}
|
|
53
|
-
/** Read one terminal control sequence so non-printing ANSI never takes a cell. */
|
|
54
|
-
function readEscape(line, start) {
|
|
55
|
-
const kind = line[start + 1];
|
|
56
|
-
if (kind === undefined)
|
|
57
|
-
return undefined;
|
|
58
|
-
// OSC, APC, DCS, and PM are string controls terminated by BEL or ST. pi-tui's
|
|
59
|
-
// cursor marker uses APC, so it must consume no display cells here.
|
|
60
|
-
if (kind === ']' || kind === '_' || kind === 'P' || kind === '^') {
|
|
61
|
-
const bel = line.indexOf('\x07', start + 2);
|
|
62
|
-
const st = line.indexOf('\x1b\\', start + 2);
|
|
63
|
-
const end = bel === -1 ? st : st === -1 ? bel : Math.min(bel, st);
|
|
64
|
-
if (end === -1)
|
|
65
|
-
return undefined;
|
|
66
|
-
return { text: line.slice(start, end + (end === bel ? 1 : 2)), end: end + (end === bel ? 1 : 2) };
|
|
67
|
-
}
|
|
68
|
-
if (kind === '[') {
|
|
69
|
-
let end = start + 2;
|
|
70
|
-
while (end < line.length && (line.charCodeAt(end) < 0x40 || line.charCodeAt(end) > 0x7e))
|
|
71
|
-
end++;
|
|
72
|
-
if (end >= line.length)
|
|
73
|
-
return undefined;
|
|
74
|
-
return { text: line.slice(start, end + 1), end: end + 1 };
|
|
75
|
-
}
|
|
76
|
-
return { text: line.slice(start, start + 2), end: start + 2 };
|
|
32
|
+
},
|
|
33
|
+
onGlyph: (glyph) => {
|
|
34
|
+
if (uri !== undefined || hitColumn === undefined || activeUri === undefined || glyph.width === 0)
|
|
35
|
+
return;
|
|
36
|
+
const cell = hitColumn - 1;
|
|
37
|
+
if (cell >= glyph.cell && cell < glyph.cell + glyph.width)
|
|
38
|
+
uri = activeUri;
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
return { activeUri, uri };
|
|
77
42
|
}
|
|
78
43
|
/** `null` means another escape sequence; an empty URI closes the active link. */
|
|
79
44
|
function parseOsc8(sequence) {
|
|
@@ -25,9 +25,13 @@ export declare class ViewerFrame implements Component {
|
|
|
25
25
|
private readonly parts;
|
|
26
26
|
private viewportRows;
|
|
27
27
|
private lastFrame;
|
|
28
|
+
private overlay;
|
|
28
29
|
constructor(terminal: Terminal, parts: ViewerFrameParts);
|
|
29
30
|
render(width: number): string[];
|
|
30
|
-
/**
|
|
31
|
+
/** Paint attributes over the composed frame — mouse selection highlight. The
|
|
32
|
+
* overlay may restyle rows but must return the same number of them. */
|
|
33
|
+
setOverlay(overlay: (frame: readonly string[]) => string[]): void;
|
|
34
|
+
/** The physical rows returned by the most recent render, before any overlay. */
|
|
31
35
|
lastRenderedFrame(): readonly string[];
|
|
32
36
|
invalidate(): void;
|
|
33
37
|
/** Rows the transcript viewport got in the last frame — the page size. */
|
|
@@ -56,6 +56,7 @@ export class ViewerFrame {
|
|
|
56
56
|
parts;
|
|
57
57
|
viewportRows = 0;
|
|
58
58
|
lastFrame = [];
|
|
59
|
+
overlay;
|
|
59
60
|
constructor(terminal, parts) {
|
|
60
61
|
this.terminal = terminal;
|
|
61
62
|
this.parts = parts;
|
|
@@ -117,12 +118,26 @@ export class ViewerFrame {
|
|
|
117
118
|
if (frame.length !== rows)
|
|
118
119
|
reportInvariant(`frame correction rendered ${frame.length} rows; expected ${rows}`);
|
|
119
120
|
const finalFrame = this.ensureCursorMarker(frame, editorLines, editorSlice, editorBottomOffset, bottomLines.length, rows);
|
|
120
|
-
// This is the exact physical screen content used for OSC-8 mouse hit tests
|
|
121
|
-
//
|
|
121
|
+
// This is the exact physical screen content used for OSC-8 mouse hit tests
|
|
122
|
+
// and for the text a mouse selection copies, so it is kept BEFORE the
|
|
123
|
+
// overlay paints attributes over it. Keep a copy because terminal rendering
|
|
124
|
+
// is allowed to retain the returned array.
|
|
122
125
|
this.lastFrame = finalFrame.slice();
|
|
123
|
-
|
|
126
|
+
if (this.overlay === undefined)
|
|
127
|
+
return finalFrame;
|
|
128
|
+
const painted = this.overlay(finalFrame);
|
|
129
|
+
if (painted.length !== finalFrame.length) {
|
|
130
|
+
reportInvariant(`frame overlay returned ${painted.length} rows; expected ${finalFrame.length}`);
|
|
131
|
+
return finalFrame;
|
|
132
|
+
}
|
|
133
|
+
return painted;
|
|
134
|
+
}
|
|
135
|
+
/** Paint attributes over the composed frame — mouse selection highlight. The
|
|
136
|
+
* overlay may restyle rows but must return the same number of them. */
|
|
137
|
+
setOverlay(overlay) {
|
|
138
|
+
this.overlay = overlay;
|
|
124
139
|
}
|
|
125
|
-
/** The physical rows returned by the most recent render. */
|
|
140
|
+
/** The physical rows returned by the most recent render, before any overlay. */
|
|
126
141
|
lastRenderedFrame() {
|
|
127
142
|
return this.lastFrame;
|
|
128
143
|
}
|
|
@@ -36,7 +36,7 @@ export function createInputController(ctx, km, capabilities, hooks) {
|
|
|
36
36
|
//
|
|
37
37
|
// A read-only viewer wires NEITHER. A REMOTE attach (`--canvas`) disables both
|
|
38
38
|
// because they target the LOCAL `auth.json` via `defaultAgentDir()`, the wrong
|
|
39
|
-
// host for a remote node
|
|
39
|
+
// host for a remote node; an OBSERVER because
|
|
40
40
|
// switching credentials and telling the broker to reload them mutates the
|
|
41
41
|
// engine's auth, which a read-only viewer must not do. Unwired here means the
|
|
42
42
|
// slash context carries no picker at all, so `/login` and `/logout` report
|
|
@@ -70,7 +70,7 @@ export function createInputController(ctx, km, capabilities, hooks) {
|
|
|
70
70
|
onCopyTarget: hooks.copyTarget,
|
|
71
71
|
onToggleMouse: () => {
|
|
72
72
|
const on = hooks.toggleMouse();
|
|
73
|
-
setNotice(on ? 'Mouse: wheel scrolling on' : 'Mouse: off — tmux drag-select and copy-mode restored');
|
|
73
|
+
setNotice(on ? 'Mouse: wheel scrolling on, drag to select and copy' : 'Mouse: off — tmux drag-select and copy-mode restored');
|
|
74
74
|
},
|
|
75
75
|
onColor: hooks.setChipColor,
|
|
76
76
|
onPromptSent: hooks.onPromptSent,
|
|
@@ -18,6 +18,10 @@ export interface MouseHandlers {
|
|
|
18
18
|
onWheel(direction: 'up' | 'down', lines: number, column: number): void;
|
|
19
19
|
/** A primary-button press with no Shift, Alt, or Control modifier. */
|
|
20
20
|
onPrimaryPress(column: number, row: number): void;
|
|
21
|
+
/** Pointer motion with the primary button held. */
|
|
22
|
+
onPrimaryDrag(column: number, row: number): void;
|
|
23
|
+
/** The primary button came back up, wherever the pointer ended. */
|
|
24
|
+
onPrimaryRelease(column: number, row: number): void;
|
|
21
25
|
}
|
|
22
26
|
/** Create per-viewer mouse reporting from the user's saved starting mode. */
|
|
23
27
|
export declare function createMouseReporting(write: (data: string) => void, defaultMode: MouseModeDefault): MouseReporting;
|
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
// Mouse reporting starts from the user's configured default. Follow-tmux mode
|
|
4
4
|
// leaves native terminal selection alone when tmux mouse is off; inside a
|
|
5
5
|
// reporting viewer, every mouse sequence is consumed so clicks and drags can
|
|
6
|
-
// never become editor input
|
|
6
|
+
// never become editor input — which is why the viewer, not the terminal, owns
|
|
7
|
+
// drag-select while reporting is on (session/selection.ts).
|
|
8
|
+
//
|
|
9
|
+
// Mode 1002 is button-event tracking: press, motion while a button is held, and
|
|
10
|
+
// release. Those three are exactly the gesture parts a selection needs, so the
|
|
11
|
+
// drag is reconstructed here and reported as press · drag · release.
|
|
7
12
|
import { spawnSync } from 'node:child_process';
|
|
8
13
|
import { inTmux } from '../../../core/runtime/placement-tmux.js';
|
|
9
14
|
const MOUSE_REPORTING_ON = '\x1b[?1002h\x1b[?1006h';
|
|
@@ -46,12 +51,18 @@ export function createMouseReporting(write, defaultMode) {
|
|
|
46
51
|
const withoutSgr = data.replace(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/g, (_match, buttons, column, row, action) => {
|
|
47
52
|
consumed = true;
|
|
48
53
|
const code = Number(buttons);
|
|
54
|
+
// Bits 4/8/16 are Shift/Meta/Control; bit 32 is the motion flag, which
|
|
55
|
+
// must survive the mask so a drag is distinguishable from a press.
|
|
49
56
|
const button = code & ~(4 | 8 | 16);
|
|
50
57
|
if (button === 64)
|
|
51
58
|
handlers.onWheel('up', WHEEL_LINES, Number(column));
|
|
52
|
-
if (button === 65)
|
|
59
|
+
else if (button === 65)
|
|
53
60
|
handlers.onWheel('down', WHEEL_LINES, Number(column));
|
|
54
|
-
if (
|
|
61
|
+
else if (button === 32 && action === 'M')
|
|
62
|
+
handlers.onPrimaryDrag(Number(column), Number(row));
|
|
63
|
+
else if (button === 0 && action === 'm')
|
|
64
|
+
handlers.onPrimaryRelease(Number(column), Number(row));
|
|
65
|
+
else if (code === 0 && action === 'M')
|
|
55
66
|
handlers.onPrimaryPress(Number(column), Number(row));
|
|
56
67
|
return '';
|
|
57
68
|
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type SelectionRelease = {
|
|
2
|
+
kind: 'click';
|
|
3
|
+
column: number;
|
|
4
|
+
row: number;
|
|
5
|
+
} | {
|
|
6
|
+
kind: 'selection';
|
|
7
|
+
notice: string;
|
|
8
|
+
};
|
|
9
|
+
export interface FrameSelection {
|
|
10
|
+
/** Primary button pressed: anchor here and drop any previous highlight. */
|
|
11
|
+
begin(column: number, row: number): void;
|
|
12
|
+
/** Drag to a new cell. Returns true when the highlight changed. */
|
|
13
|
+
extend(column: number, row: number): boolean;
|
|
14
|
+
/** Primary button released. `click` means the press never moved, so the
|
|
15
|
+
* caller's click handling (file links) still owns the gesture. */
|
|
16
|
+
release(): SelectionRelease | undefined;
|
|
17
|
+
/** Drop the highlight. Returns true when something was showing. */
|
|
18
|
+
clear(): boolean;
|
|
19
|
+
/** A highlight is on screen. */
|
|
20
|
+
active(): boolean;
|
|
21
|
+
/** Paint the highlight onto a composed frame; identity when nothing is selected. */
|
|
22
|
+
apply(frame: readonly string[]): string[];
|
|
23
|
+
}
|
|
24
|
+
export declare function createFrameSelection(deps: {
|
|
25
|
+
/** The physical rows of the last render, unhighlighted. */
|
|
26
|
+
frame: () => readonly string[];
|
|
27
|
+
width: () => number;
|
|
28
|
+
}): FrameSelection;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// session/selection.ts — mouse drag-select and copy over the rendered frame.
|
|
2
|
+
//
|
|
3
|
+
// While the viewer reports mouse events, the terminal hands presses and drags to
|
|
4
|
+
// this process, so the terminal's own drag-select is unavailable: the viewer has
|
|
5
|
+
// to do the selecting. It selects the way a terminal does — over the physical
|
|
6
|
+
// rows last handed to the screen, not over transcript records — so wrapping,
|
|
7
|
+
// folds, the pinned header, and the cockpit all select exactly as they look.
|
|
8
|
+
// Releasing the button copies the selection to the clipboard (and, in tmux, the
|
|
9
|
+
// paste buffer); the highlight stays up as confirmation until the next keypress,
|
|
10
|
+
// wheel notch, or press.
|
|
11
|
+
import { highlightCells, sliceCells } from '../ansi-cells.js';
|
|
12
|
+
import { writeClipboardTextEverywhere } from '../../../core/clipboard-text.js';
|
|
13
|
+
function before(a, b) {
|
|
14
|
+
return a.row < b.row || (a.row === b.row && a.column <= b.column);
|
|
15
|
+
}
|
|
16
|
+
/** The per-row cell ranges covered by a drag, in terminal selection geometry:
|
|
17
|
+
* the first row runs to the pane edge, whole rows in between, the last row up
|
|
18
|
+
* to and including the cell under the pointer. */
|
|
19
|
+
function spans(anchor, head, width) {
|
|
20
|
+
const [start, end] = before(anchor, head) ? [anchor, head] : [head, anchor];
|
|
21
|
+
const rows = [];
|
|
22
|
+
for (let row = start.row; row <= end.row; row++) {
|
|
23
|
+
const from = row === start.row ? start.column - 1 : 0;
|
|
24
|
+
const to = row === end.row ? end.column : width;
|
|
25
|
+
if (to > from)
|
|
26
|
+
rows.push({ row, from, to });
|
|
27
|
+
}
|
|
28
|
+
return rows;
|
|
29
|
+
}
|
|
30
|
+
export function createFrameSelection(deps) {
|
|
31
|
+
let anchor;
|
|
32
|
+
let head;
|
|
33
|
+
const selected = () => anchor !== undefined && head !== undefined ? { anchor, head } : undefined;
|
|
34
|
+
const selectionText = () => {
|
|
35
|
+
const current = selected();
|
|
36
|
+
if (!current)
|
|
37
|
+
return '';
|
|
38
|
+
const frame = deps.frame();
|
|
39
|
+
const rows = spans(current.anchor, current.head, deps.width())
|
|
40
|
+
.map((span) => sliceCells(frame[span.row - 1] ?? '', span.from, span.to).trimEnd());
|
|
41
|
+
// A trailing blank row is an artifact of where the button came up; leading
|
|
42
|
+
// blanks inside the span are real content the user dragged across.
|
|
43
|
+
while (rows.length > 0 && rows[rows.length - 1] === '')
|
|
44
|
+
rows.pop();
|
|
45
|
+
return rows.join('\n');
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
begin(column, row) {
|
|
49
|
+
anchor = { row, column };
|
|
50
|
+
head = undefined;
|
|
51
|
+
},
|
|
52
|
+
extend(column, row) {
|
|
53
|
+
if (anchor === undefined)
|
|
54
|
+
return false;
|
|
55
|
+
if (head !== undefined && head.row === row && head.column === column)
|
|
56
|
+
return false;
|
|
57
|
+
head = { row, column };
|
|
58
|
+
return true;
|
|
59
|
+
},
|
|
60
|
+
release() {
|
|
61
|
+
const press = anchor;
|
|
62
|
+
if (press === undefined)
|
|
63
|
+
return undefined;
|
|
64
|
+
if (head === undefined) {
|
|
65
|
+
anchor = undefined;
|
|
66
|
+
return { kind: 'click', column: press.column, row: press.row };
|
|
67
|
+
}
|
|
68
|
+
const text = selectionText();
|
|
69
|
+
if (text === '') {
|
|
70
|
+
anchor = undefined;
|
|
71
|
+
head = undefined;
|
|
72
|
+
return { kind: 'selection', notice: 'Nothing to copy — selection is empty' };
|
|
73
|
+
}
|
|
74
|
+
const { system, tmux } = writeClipboardTextEverywhere(text);
|
|
75
|
+
const destination = system && tmux ? 'clipboard + tmux buffer' : system ? 'clipboard' : tmux ? 'tmux buffer' : undefined;
|
|
76
|
+
const lines = text.split('\n').length;
|
|
77
|
+
return {
|
|
78
|
+
kind: 'selection',
|
|
79
|
+
notice: destination === undefined
|
|
80
|
+
? 'Copy failed — no clipboard tool found'
|
|
81
|
+
: `Copied selection (${lines} ${lines === 1 ? 'line' : 'lines'}) — ${destination}`,
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
clear() {
|
|
85
|
+
const showing = head !== undefined;
|
|
86
|
+
anchor = undefined;
|
|
87
|
+
head = undefined;
|
|
88
|
+
return showing;
|
|
89
|
+
},
|
|
90
|
+
active() {
|
|
91
|
+
return head !== undefined;
|
|
92
|
+
},
|
|
93
|
+
apply(frame) {
|
|
94
|
+
const current = selected();
|
|
95
|
+
if (!current)
|
|
96
|
+
return frame.slice();
|
|
97
|
+
const width = deps.width();
|
|
98
|
+
const painted = frame.slice();
|
|
99
|
+
for (const span of spans(current.anchor, current.head, width)) {
|
|
100
|
+
const index = span.row - 1;
|
|
101
|
+
if (index < 0 || index >= painted.length)
|
|
102
|
+
continue;
|
|
103
|
+
painted[index] = highlightCells(painted[index], span.from, span.to, width);
|
|
104
|
+
}
|
|
105
|
+
return painted;
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -57,18 +57,16 @@ export function createTeardown(s, hooks) {
|
|
|
57
57
|
socket.close();
|
|
58
58
|
restore();
|
|
59
59
|
// A clean detach leaves the broker running (the engine runs on) — the
|
|
60
|
-
// documented "detach leaves the broker running" contract
|
|
61
|
-
// reaping
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
// API-only client cannot reach and no per-node reap route exposes, so it is
|
|
65
|
-
// dropped (B-10). Disposition of an abandoned empty broker: `crtr canvas
|
|
60
|
+
// documented "detach leaves the broker running" contract. Teardown reaps
|
|
61
|
+
// nothing per-action: reaping an empty husk here would need a direct
|
|
62
|
+
// canvas.db mutation the API-only client cannot reach, and no per-node reap
|
|
63
|
+
// route exposes one. Disposition of an abandoned empty broker: `crtr canvas
|
|
66
64
|
// prune --empty` (reapEmptyNodes) sweeps it, but only ONCE its pid is dead
|
|
67
65
|
// — it deliberately spares a LIVE broker as the daemon's domain. So an empty
|
|
68
66
|
// broker lingers until it idle-releases (or is closed) to a dormant husk,
|
|
69
|
-
// then prune reaps it
|
|
70
|
-
//
|
|
71
|
-
//
|
|
67
|
+
// then prune reaps it, leaving a narrow, transient live-empty window — the
|
|
68
|
+
// only faithful option under the API boundary. A per-node reap route would
|
|
69
|
+
// buy immediate cleanup (future work).
|
|
72
70
|
if (reason === 'broker-gone') {
|
|
73
71
|
const msg = hooks.lastSocketError() ??
|
|
74
72
|
`broker gone — node ${nodeId} is no longer running its engine. Re-focus to reattach.`;
|
|
@@ -87,8 +87,7 @@ export declare const CANVAS_SLASH_COMMANDS: ReadonlyArray<{
|
|
|
87
87
|
* `true` from `dispatchSlashCommand` — swallowed with a notice, never
|
|
88
88
|
* forwarded as prose — so it counts as consumed here too, independent of
|
|
89
89
|
* `remote`). Exactly BUILTIN_NAMES ∪ CANVAS_NAMES ∪ SCOPED_OUT — every name
|
|
90
|
-
* `dispatchSlashCommand` returns `true` for
|
|
91
|
-
* references, review Major 6/terminal). Used ONLY for terminal
|
|
90
|
+
* `dispatchSlashCommand` returns `true` for. Used ONLY for terminal
|
|
92
91
|
* ref-decoration suppression (`input/titled-editor.ts`); never merged into
|
|
93
92
|
* command completion, which is `slashCommandList` — a different,
|
|
94
93
|
* remote-filtered, broker-augmented list. */
|
|
@@ -19,12 +19,13 @@
|
|
|
19
19
|
// against pi 0.79.1 — `ScopedModelsSelectorComponent` is the one selector pi does
|
|
20
20
|
// not re-export, so /scoped-models renders read-only via a SelectList.)
|
|
21
21
|
//
|
|
22
|
-
// SCOPED OUT (
|
|
22
|
+
// SCOPED OUT (no engine method): `/login`, `/logout`, `/share`,
|
|
23
23
|
// `/trust` — a brief "not supported in attach" notice, never a frame.
|
|
24
24
|
import { execFile } from 'node:child_process';
|
|
25
25
|
import { BUILTIN_SLASH_COMMANDS } from '../../../core/runtime/pi-vendored.js';
|
|
26
26
|
import { surfaceTmuxStyleArgs } from '../../../core/runtime/surface-bg.js';
|
|
27
27
|
import { viewerOperationForSlash } from '../input/capabilities.js';
|
|
28
|
+
import { shellQuote } from '../../../shared/shell-quote.js';
|
|
28
29
|
/** Canvas slash-commands — implemented NATIVE in the viewer, the ONLY live host:
|
|
29
30
|
* the broker binds mode:'rpc', so broker pi-extensions never run their
|
|
30
31
|
* interactive (mode==='tui') paths. `/promote` triggers a turn after shelling
|
|
@@ -43,7 +44,7 @@ export const CANVAS_SLASH_COMMANDS = [
|
|
|
43
44
|
{ name: 'mcp', description: 'MCP servers — status from the adapter’s config/cache; Enter (or /mcp <server>) connects, reconnects, or re-auths' },
|
|
44
45
|
{ name: 'rename', description: 'Rename this node everywhere — node row, viewer window, session chip, tmux pane: /rename <name>' },
|
|
45
46
|
{ name: 'color', description: 'Color this node — tmux pane outline + name chip: /color <color> (red, blue, #rrggbb, …) or /color none' },
|
|
46
|
-
{ name: 'mouse', description: 'Toggle mouse wheel scrolling — off restores tmux drag-select and copy-mode mouse' },
|
|
47
|
+
{ name: 'mouse', description: 'Toggle mouse wheel scrolling and drag-select — off restores tmux drag-select and copy-mode mouse' },
|
|
47
48
|
{ name: 'copy-message', description: 'Copy the newest message visible in the transcript viewport' },
|
|
48
49
|
{ name: 'copy-tool', description: 'Copy the newest tool call output' },
|
|
49
50
|
{ name: 'copy-transcript', description: 'Copy every copyable transcript block' },
|
|
@@ -58,7 +59,7 @@ const TMUX_NAMED_COLORS = new Set([
|
|
|
58
59
|
'brightred', 'brightgreen', 'brightyellow', 'brightblue', 'brightmagenta', 'brightcyan',
|
|
59
60
|
'orange', 'grey', 'gray', 'default',
|
|
60
61
|
]);
|
|
61
|
-
/** Builtins with no
|
|
62
|
+
/** Builtins with no engine method — scoped out. */
|
|
62
63
|
const SCOPED_OUT = new Set(['trust']);
|
|
63
64
|
/** Valid `/settings thinking` levels — the `SetThinkingLevelFrame['level']`
|
|
64
65
|
* union (= AgentSession['thinkingLevel'], pi-agent-core ThinkingLevel),
|
|
@@ -92,8 +93,7 @@ function readOnlyRefusal(name, ctx) {
|
|
|
92
93
|
* `true` from `dispatchSlashCommand` — swallowed with a notice, never
|
|
93
94
|
* forwarded as prose — so it counts as consumed here too, independent of
|
|
94
95
|
* `remote`). Exactly BUILTIN_NAMES ∪ CANVAS_NAMES ∪ SCOPED_OUT — every name
|
|
95
|
-
* `dispatchSlashCommand` returns `true` for
|
|
96
|
-
* references, review Major 6/terminal). Used ONLY for terminal
|
|
96
|
+
* `dispatchSlashCommand` returns `true` for. Used ONLY for terminal
|
|
97
97
|
* ref-decoration suppression (`input/titled-editor.ts`); never merged into
|
|
98
98
|
* command completion, which is `slashCommandList` — a different,
|
|
99
99
|
* remote-filtered, broker-augmented list. */
|
|
@@ -605,10 +605,6 @@ function popup(cmd) {
|
|
|
605
605
|
/* best-effort */
|
|
606
606
|
}
|
|
607
607
|
}
|
|
608
|
-
/** Single-quote a string for safe interpolation into a `sh -c` command line. */
|
|
609
|
-
function shellQuote(s) {
|
|
610
|
-
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
611
|
-
}
|
|
612
608
|
// ---------------------------------------------------------------------------
|
|
613
609
|
function usage(ctx, form) {
|
|
614
610
|
ctx.notify(`Usage: ${form}`);
|