@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.
Files changed (152) hide show
  1. package/dist/api/client.d.ts +4 -4
  2. package/dist/api/client.js +2 -2
  3. package/dist/api/dto/lifecycle.d.ts +1 -1
  4. package/dist/api/dto/reports.d.ts +1 -1
  5. package/dist/clients/attach/__tests__/frame-selection.test.d.ts +1 -0
  6. package/dist/clients/attach/__tests__/frame-selection.test.js +84 -0
  7. package/dist/clients/attach/__tests__/group-activity.test.js +2 -2
  8. package/dist/clients/attach/ansi-cells.d.ts +31 -0
  9. package/dist/clients/attach/ansi-cells.js +120 -0
  10. package/dist/clients/attach/config.js +5 -5
  11. package/dist/clients/attach/input/clipboard-image.js +2 -2
  12. package/dist/clients/attach/input/ref-autocomplete.js +1 -1
  13. package/dist/clients/attach/input/titled-editor.d.ts +12 -13
  14. package/dist/clients/attach/input/titled-editor.js +14 -15
  15. package/dist/clients/attach/overlays/dialogs.d.ts +1 -1
  16. package/dist/clients/attach/overlays/dialogs.js +2 -2
  17. package/dist/clients/attach/overlays/help.js +2 -1
  18. package/dist/clients/attach/overlays/mcp.js +5 -12
  19. package/dist/clients/attach/render/chat-view.d.ts +1 -1
  20. package/dist/clients/attach/render/chat-view.js +2 -2
  21. package/dist/clients/attach/render/group-recap.js +7 -0
  22. package/dist/clients/attach/session/context.js +3 -3
  23. package/dist/clients/attach/session/editor-inventory.js +6 -6
  24. package/dist/clients/attach/session/file-links.js +15 -50
  25. package/dist/clients/attach/session/frame.d.ts +5 -1
  26. package/dist/clients/attach/session/frame.js +19 -4
  27. package/dist/clients/attach/session/input-wiring.js +2 -2
  28. package/dist/clients/attach/session/mouse.d.ts +4 -0
  29. package/dist/clients/attach/session/mouse.js +14 -3
  30. package/dist/clients/attach/session/selection.d.ts +28 -0
  31. package/dist/clients/attach/session/selection.js +108 -0
  32. package/dist/clients/attach/session/teardown.js +7 -9
  33. package/dist/clients/attach/slash/dispatch.d.ts +1 -2
  34. package/dist/clients/attach/slash/dispatch.js +5 -9
  35. package/dist/clients/attach/viewer.js +552 -550
  36. package/dist/clients/inbox/controller.js +3 -2
  37. package/dist/clients/inbox/deck-adapter.js +3 -2
  38. package/dist/clients/surfaces/host.d.ts +1 -1
  39. package/dist/commands/api-client.d.ts +12 -13
  40. package/dist/commands/api-client.js +15 -16
  41. package/dist/commands/chord.js +1 -2
  42. package/dist/commands/human/queue.js +6 -5
  43. package/dist/commands/memory/find.js +3 -3
  44. package/dist/commands/node/lifecycle.js +1 -1
  45. package/dist/commands/node-lifecycle-revive.d.ts +1 -1
  46. package/dist/commands/node-lifecycle-revive.js +3 -3
  47. package/dist/commands/push.js +1 -1
  48. package/dist/commands/surface/node/placement.d.ts +1 -1
  49. package/dist/commands/surface/node/placement.js +1 -1
  50. package/dist/commands/surface-edit.js +1 -4
  51. package/dist/commands/sys/panels/panel.d.ts +3 -3
  52. package/dist/commands/sys/panels/panel.js +3 -3
  53. package/dist/commands/sys/setup-core.js +1 -7
  54. package/dist/commands/sys/sync-deps.js +1 -3
  55. package/dist/commands/sys/sync-project-guidance.js +2 -11
  56. package/dist/commands/sys/sysprompt.js +1 -3
  57. package/dist/core/__tests__/fixtures/fake-engine.js +1 -1
  58. package/dist/core/__tests__/helpers/harness.d.ts +1 -1
  59. package/dist/core/__tests__/helpers/harness.js +1 -1
  60. package/dist/core/canvas/boot.js +1 -1
  61. package/dist/core/canvas/browse/app.js +2 -2
  62. package/dist/core/canvas/canvas.d.ts +3 -3
  63. package/dist/core/canvas/canvas.js +3 -3
  64. package/dist/core/canvas/daemon-owner.js +1 -2
  65. package/dist/core/canvas/db.js +7 -8
  66. package/dist/core/canvas/index.js +2 -2
  67. package/dist/core/canvas/pid.d.ts +12 -14
  68. package/dist/core/canvas/pid.js +16 -20
  69. package/dist/core/canvas/remote-transport.js +2 -2
  70. package/dist/core/canvas/status-glyph.d.ts +1 -1
  71. package/dist/core/canvas/status-glyph.js +1 -1
  72. package/dist/core/canvas/types.d.ts +1 -1
  73. package/dist/core/command-manifests/manifest.js +4 -6
  74. package/dist/core/command-manifests/schema.js +10 -12
  75. package/dist/core/command-plugins/transport/exec-invoke.js +5 -7
  76. package/dist/core/command-plugins/transport/http-invoke.js +4 -6
  77. package/dist/core/config.d.ts +1 -1
  78. package/dist/core/config.js +2 -2
  79. package/dist/core/events/read.js +1 -3
  80. package/dist/core/fault-classifier.js +1 -3
  81. package/dist/core/fs-utils.d.ts +7 -0
  82. package/dist/core/fs-utils.js +29 -2
  83. package/dist/core/host-exports/export.d.ts +4 -5
  84. package/dist/core/host-exports/export.js +4 -5
  85. package/dist/core/human/claim.js +3 -2
  86. package/dist/core/human/convention.d.ts +0 -1
  87. package/dist/core/human/convention.js +1 -9
  88. package/dist/core/human/scan.js +5 -4
  89. package/dist/core/profiles/manifest.d.ts +1 -1
  90. package/dist/core/profiles/manifest.js +1 -1
  91. package/dist/core/review/types.d.ts +1 -1
  92. package/dist/core/review/types.js +1 -1
  93. package/dist/core/runtime/broker/frame-dispatch.js +4 -4
  94. package/dist/core/runtime/broker-cli.js +1 -1
  95. package/dist/core/runtime/broker-protocol.d.ts +9 -10
  96. package/dist/core/runtime/broker-protocol.js +9 -10
  97. package/dist/core/runtime/broker-sdk.d.ts +1 -1
  98. package/dist/core/runtime/broker.js +6 -6
  99. package/dist/core/runtime/host.js +14 -14
  100. package/dist/core/runtime/launch.d.ts +1 -1
  101. package/dist/core/runtime/launch.js +1 -1
  102. package/dist/core/runtime/lifecycle.js +16 -21
  103. package/dist/core/runtime/node-read.js +2 -3
  104. package/dist/core/runtime/package-health.js +1 -8
  105. package/dist/core/runtime/persona.js +2 -2
  106. package/dist/core/runtime/pi-vendored.d.ts +4 -2
  107. package/dist/core/runtime/pi-vendored.js +5 -16
  108. package/dist/core/runtime/promote.js +1 -1
  109. package/dist/core/runtime/reopen.js +2 -2
  110. package/dist/core/runtime/revive-all.d.ts +3 -3
  111. package/dist/core/runtime/revive-all.js +6 -6
  112. package/dist/core/runtime/revive.js +1 -1
  113. package/dist/core/runtime/spawn.d.ts +1 -2
  114. package/dist/core/runtime/spawn.js +2 -3
  115. package/dist/core/runtime/structured-output.js +4 -6
  116. package/dist/core/runtime/tmux-driver.d.ts +2 -2
  117. package/dist/core/runtime/tmux-driver.js +3 -5
  118. package/dist/core/runtime/tool-group-summary.js +5 -4
  119. package/dist/core/runtime/warm-pool.js +1 -1
  120. package/dist/core/scope.js +2 -9
  121. package/dist/core/self-update.js +3 -3
  122. package/dist/core/spawn.d.ts +0 -1
  123. package/dist/core/spawn.js +1 -4
  124. package/dist/core/substrate/on-read.js +1 -9
  125. package/dist/core/wake.js +2 -2
  126. package/dist/core/worktree.d.ts +3 -3
  127. package/dist/core/worktree.js +5 -7
  128. package/dist/daemon/api/bridge.js +1 -1
  129. package/dist/daemon/api/handlers/broker-ops.js +1 -2
  130. package/dist/daemon/api/handlers/messages.js +2 -2
  131. package/dist/daemon/api/handlers/nodes.js +1 -1
  132. package/dist/daemon/api/handlers/reports.js +1 -1
  133. package/dist/daemon/api/handlers/validate.d.ts +2 -1
  134. package/dist/daemon/api/handlers/validate.js +2 -3
  135. package/dist/daemon/cron-run.js +7 -7
  136. package/dist/daemon/human/finish.js +3 -2
  137. package/dist/daemon/manage.d.ts +6 -6
  138. package/dist/daemon/manage.js +15 -16
  139. package/dist/pi-extensions/canvas-context-intro.js +1 -14
  140. package/dist/pi-extensions/canvas-doc-substrate.js +1 -1
  141. package/dist/pi-extensions/canvas-review-boundary.js +1 -14
  142. package/dist/pi-extensions/truncate.d.ts +5 -0
  143. package/dist/pi-extensions/truncate.js +14 -0
  144. package/dist/shared/predicates.d.ts +2 -0
  145. package/dist/shared/predicates.js +4 -0
  146. package/dist/shared/shell-quote.d.ts +3 -0
  147. package/dist/shared/shell-quote.js +5 -0
  148. package/dist/shared/tool-groups.d.ts +4 -2
  149. package/dist/shared/tool-groups.js +3 -3
  150. package/dist/types.d.ts +4 -5
  151. package/package.json +1 -1
  152. 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
- // `runAttach` used to hold every one of these as a closure local, which is why
4
- // pulling any block out of it meant inventing a bespoke parameter list for that
5
- // block alone. `AttachSession` gives the state one declaration; each
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 (design: inline memory references, review Major 6) — leading tokens
66
- // the ENGINE independently consumes, kept separate from the viewer-local
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 (T3
86
- // ownership note: this module combines command metadata, ref metadata,
87
- // provider rebuild, and editor highlight sets in one place). Command-only
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 (review Major 6): the viewer-local
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 stringWidth from 'string-width';
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 cell = 1;
28
- let index = 0;
29
- while (index < line.length) {
30
- if (line[index] === '\x1b') {
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
- index = escape.end;
38
- continue;
39
- }
40
- const nextEscape = line.indexOf('\x1b', index);
41
- const text = line.slice(index, nextEscape === -1 ? undefined : nextEscape);
42
- for (const segment of GRAPHEMES.segment(text)) {
43
- const width = stringWidth(segment.segment);
44
- if (hitColumn !== undefined && width > 0 && hitColumn >= cell && hitColumn < cell + width && activeUri !== undefined) {
45
- return { activeUri, uri: activeUri };
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
- /** The physical rows returned by the most recent render. */
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
- // Keep a copy because terminal rendering is allowed to retain the returned array.
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
- return finalFrame;
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 (Phase 3 review Major 1, 1d); an OBSERVER because
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 (code === 0 && action === 'M')
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 (B2). Per-action
61
- // reaping of an empty husk used to run here (reapIfEmpty →
62
- // teardown/deleteNode, which reaps EVEN a live broker since the user
63
- // explicitly detached), but it required a direct canvas.db mutation the
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. Net vs OLD: a narrow, transient live-empty window
70
- // replaces immediate reap — the only faithful option under the API boundary;
71
- // a per-node reap route would restore immediate cleanup (future work).
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 (design: inline memory
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 (review m2 — no engine method): `/login`, `/logout`, `/share`,
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 Phase-4 engine method — scoped out (review m2). */
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 (design: inline memory
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}`);