@timurproko/a1 0.1.8-dev.563 → 0.1.8-dev.573

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 (71) hide show
  1. package/README.md +13 -11
  2. package/dist/app/session-shell/prompt-chips.js +20 -28
  3. package/dist/app/session-shell/session-shell-root.d.ts +2 -0
  4. package/dist/app/session-shell/session-shell-root.js +60 -17
  5. package/dist/app/session-shell/session-shell.js +35 -1
  6. package/dist/app/session-shell/session-viewport-controller.d.ts +1 -0
  7. package/dist/app/session-shell/session-viewport-controller.js +52 -30
  8. package/dist/cli/dispatch.d.ts +3 -1
  9. package/dist/cli/dispatch.js +56 -56
  10. package/dist/cli/package-messages.d.ts +4 -3
  11. package/dist/cli/package-messages.js +23 -11
  12. package/dist/contracts/owned-ui/index.d.ts +2 -0
  13. package/dist/contracts/owned-ui/index.js +1 -0
  14. package/dist/contracts/owned-ui/prompt-chips.d.ts +15 -0
  15. package/dist/contracts/owned-ui/prompt-chips.js +59 -0
  16. package/dist/contracts/owned-ui/validation.js +1 -1
  17. package/dist/integrations/pi/components/index.d.ts +1 -0
  18. package/dist/integrations/pi/components/index.js +1 -0
  19. package/dist/integrations/pi/components/modal-frame.d.ts +41 -0
  20. package/dist/integrations/pi/components/modal-frame.js +110 -0
  21. package/dist/integrations/pi/components/models-dialog.js +19 -5
  22. package/dist/integrations/pi/components/shell-footer-status.d.ts +4 -3
  23. package/dist/integrations/pi/components/shell-footer-status.js +53 -15
  24. package/dist/integrations/pi/components/shell-selectors-dialogs.js +38 -2
  25. package/dist/integrations/pi/components/shell-shared-facade.d.ts +9 -1
  26. package/dist/integrations/pi/components/skills-dialog.js +3 -3
  27. package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +1 -1
  28. package/dist/integrations/pi/components/submitted-prompt-adapter.js +5 -2
  29. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +2 -2
  30. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +3 -2
  31. package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +2 -1
  32. package/dist/integrations/pi/components/upstream/components/extension-editor.js +7 -6
  33. package/dist/integrations/pi/components/upstream/components/extension-input.js +9 -8
  34. package/dist/integrations/pi/components/upstream/components/extension-selector.js +10 -9
  35. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
  36. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +3 -3
  37. package/dist/integrations/pi/components/upstream/components/session-selector.js +13 -6
  38. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +7 -6
  39. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +15 -12
  40. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +1 -1
  41. package/dist/integrations/pi/components/upstream/components/tree-selector.js +13 -6
  42. package/dist/integrations/pi/components/upstream/components/trust-selector.js +9 -9
  43. package/dist/integrations/pi/engine/compaction-progress.d.ts +3 -7
  44. package/dist/integrations/pi/engine/compaction-progress.js +11 -10
  45. package/dist/integrations/pi/engine/repository-pr.d.ts +3 -3
  46. package/dist/integrations/pi/engine/repository-pr.js +6 -6
  47. package/dist/integrations/pi/engine/session-runtime.js +5 -4
  48. package/dist/integrations/pi/engine/settings-bridge.js +1 -1
  49. package/dist/integrations/pi/engine/settings-port.js +1 -1
  50. package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -0
  51. package/dist/integrations/pi/tui-runtime/adapter.js +11 -2
  52. package/dist/integrations/pi/tui-runtime/contracts.d.ts +2 -0
  53. package/dist/integrations/pi/tui-runtime/mouse-report-input.d.ts +6 -0
  54. package/dist/integrations/pi/tui-runtime/mouse-report-input.js +34 -0
  55. package/dist/native/darwin-arm64/manifest.json +1 -1
  56. package/dist/native/linux-x64/manifest.json +1 -1
  57. package/dist/native/win32-x64/manifest.json +3 -3
  58. package/dist/native/win32-x64/process-guardian.exe +0 -0
  59. package/dist/ui/components/dialog-panel.js +10 -6
  60. package/dist/ui/components/index.d.ts +2 -1
  61. package/dist/ui/components/index.js +1 -1
  62. package/dist/ui/components/mouse.d.ts +2 -2
  63. package/dist/ui/components/mouse.js +2 -2
  64. package/dist/ui/components/progress-status.d.ts +9 -2
  65. package/dist/ui/components/progress-status.js +24 -3
  66. package/dist/ui/components/transcript-viewport.d.ts +9 -3
  67. package/dist/ui/components/transcript-viewport.js +116 -136
  68. package/docs/architecture/ui-reference-provenance.md +8 -5
  69. package/docs/openspec-archive-automation.md +2 -2
  70. package/docs/validation.md +1 -1
  71. package/package.json +1 -1
package/README.md CHANGED
@@ -24,13 +24,15 @@ npm install -g @timurproko/a1@0.1.8-dev.107
24
24
 
25
25
  ```sh
26
26
  a1 # launch A1 (profile: ~/.a1/agent)
27
- a1 --help # show all commands (short form: a1 -h)
28
- a1 --version # show the version (short form: a1 -v)
27
+ a1 help # show all commands
28
+ a1 version # show the version
29
29
  a1 update # install the newest stable release
30
30
  a1 update --develop # install the current development preview
31
31
  a1 update --develop 107 # install numbered preview 107
32
32
  a1 update --develop 0.1.8-dev.107 # install that exact preview
33
33
  a1 update --models # refresh A1's model catalogs
34
+ a1 update --extensions # update every installed package
35
+ a1 update npm:pi-mcp-adapter # update one package
34
36
  ```
35
37
 
36
38
  Stable builds print only their installed version. Development builds also show the
@@ -47,17 +49,17 @@ Unsupported commands exit quietly without launching anything. The removed
47
49
 
48
50
  ## Extensions
49
51
 
50
- Pi extension packages install into A1's own profile (`~/.a1/agent`), so bare `a1`
51
- loads them and `a1 pi` does not. Sources are Pi's: `npm:`, git, or a
52
- local path.
52
+ Pi-compatible extension packages install into A1's own profile (`~/.a1/agent`), so
53
+ bare `a1` loads them and the `a1 pi` comparison profile does not. Sources use Pi's
54
+ `npm:`, git, or local-path grammar.
53
55
 
54
56
  ```sh
55
- a1 pi install npm:pi-mcp-adapter # install a package
56
- a1 pi remove npm:pi-mcp-adapter # remove it (alias: a1 pi uninstall)
57
- a1 pi list # list installed packages
58
- a1 pi update --extensions # update every installed package
59
- a1 pi update npm:pi-mcp-adapter # update one
60
- a1 pi update --models # refresh A1's model catalogs
57
+ a1 install npm:pi-mcp-adapter # install a package
58
+ a1 remove npm:pi-mcp-adapter # remove it (alias: a1 uninstall)
59
+ a1 list # list installed packages
60
+ a1 update --extensions # update every installed package
61
+ a1 update npm:pi-mcp-adapter # update one
62
+ a1 update --models # refresh A1's model catalogs
61
63
  ```
62
64
 
63
65
  A1 pins the Pi runtime carried by each release, so Pi self-update forms are refused.
@@ -1,12 +1,11 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import path from "node:path";
3
3
  import { canonicalizeClipboardImage } from "./clipboard-image.js";
4
- import { assertImageEncodedSize, assertPromptImages, ImageAttachmentError } from "../../contracts/owned-ui/index.js";
4
+ import { assertImageEncodedSize, assertPromptImages, canonicalPromptChipMatches, ImageAttachmentError, replaceCanonicalPromptChips, } from "../../contracts/owned-ui/index.js";
5
5
  import { ImagePreparationClient } from "./image-preparation-client.js";
6
6
  import { preparePasteText } from "./paste-text-preparation.js";
7
7
  import { pathChipTag } from "./path-chip-presentation.js";
8
8
  import { PastePreparationClient } from "./paste-preparation-client.js";
9
- const CHIP_PATTERN = /\[(?:paste #\d+ (?:\+\d+ lines|\d+ chars)|📷 [^\]]+|📁 [^\]]+|📄 [^\]]+|🖼 {1,2}[^\]]+|🔗 [^\]]+)\]/gu;
10
9
  const URL_SUBSTRING_PATTERN = /https?:\/\/[^\s\u0000-\u001f\u007f\]]+/giu;
11
10
  const IMAGE_CHIP_IDENTIFIER_PATTERN = /^\[📷 screenshot-([a-f0-9]+)(?:-resized)?\]$/u;
12
11
  const URL_DISPLAY_LENGTH = 40;
@@ -259,10 +258,9 @@ export class PromptChipStore {
259
258
  return ranges.sort((left, right) => left.start - right.start);
260
259
  }
261
260
  atomicRanges(line) {
262
- return [...line.matchAll(CHIP_PATTERN)].filter(match => !match[0].startsWith("[paste #") || this.#chips.has(match[0])).map(match => ({
263
- start: match.index,
264
- end: match.index + match[0].length,
265
- }));
261
+ return canonicalPromptChipMatches(line)
262
+ .filter(match => !match.text.startsWith("[paste #") || this.#chips.has(match.text))
263
+ .map(match => ({ start: match.start, end: match.end }));
266
264
  }
267
265
  hyperlinkRanges(text) {
268
266
  const ranges = [];
@@ -306,8 +304,8 @@ export class PromptChipStore {
306
304
  imageChipAttachments(text) {
307
305
  const results = [];
308
306
  const seen = new Set();
309
- for (const match of text.matchAll(CHIP_PATTERN)) {
310
- const tag = match[0];
307
+ for (const match of canonicalPromptChipMatches(text)) {
308
+ const tag = match.text;
311
309
  if (seen.has(tag))
312
310
  continue;
313
311
  const chip = this.#chips.get(tag);
@@ -338,39 +336,32 @@ export class PromptChipStore {
338
336
  // Rationale: resolve image chip tags in-place before further classification. Sidecar hits
339
337
  // register a live image chip; misses silently strip the tag (no placeholder, no notice) per
340
338
  // the durability contract.
341
- CHIP_PATTERN.lastIndex = 0;
342
- const resolvedImages = text.replace(CHIP_PATTERN, tag => {
343
- const identifier = imageChipIdentifier(tag);
339
+ const resolvedImages = replaceCanonicalPromptChips(text, match => {
340
+ const identifier = imageChipIdentifier(match.text);
344
341
  if (identifier === null)
345
- return tag;
342
+ return match.text;
346
343
  const attachment = resolveImage(identifier);
347
344
  if (attachment === null)
348
345
  return "";
349
- this.#chips.set(tag, { kind: "image", tag, image: attachment });
350
- return tag;
346
+ this.#chips.set(match.text, { kind: "image", tag: match.text, image: attachment });
347
+ return match.text;
351
348
  });
352
349
  // Rationale: split around any surviving image tags so paste-time classification runs on the
353
350
  // intervening prose exactly as it would on a fresh clipboard payload, plus a substring URL
354
351
  // scan so embedded links become atomic chips even in the middle of surrounding text.
355
- CHIP_PATTERN.lastIndex = 0;
356
352
  const segments = [];
357
353
  let cursor = 0;
358
- for (const match of resolvedImages.matchAll(CHIP_PATTERN)) {
359
- const index = match.index ?? cursor;
360
- if (index > cursor)
361
- segments.push(resolvedImages.slice(cursor, index));
362
- segments.push(match[0]);
363
- cursor = index + match[0].length;
354
+ for (const match of canonicalPromptChipMatches(resolvedImages)) {
355
+ if (match.start > cursor)
356
+ segments.push(resolvedImages.slice(cursor, match.start));
357
+ segments.push(match.text);
358
+ cursor = match.end;
364
359
  }
365
360
  if (cursor < resolvedImages.length)
366
361
  segments.push(resolvedImages.slice(cursor));
367
- CHIP_PATTERN.lastIndex = 0;
368
362
  return segments.map(segment => {
369
- if (segment.length === 0 || CHIP_PATTERN.test(segment)) {
370
- CHIP_PATTERN.lastIndex = 0;
363
+ if (segment.length === 0 || canonicalPromptChipMatches(segment).length > 0)
371
364
  return segment;
372
- }
373
- CHIP_PATTERN.lastIndex = 0;
374
365
  // Invariant: preserve original whitespace framing so the concatenated recall value
375
366
  // remains stable across rehydration.
376
367
  const leading = segment.match(/^\s+/)?.[0] ?? "";
@@ -434,7 +425,8 @@ export class PromptChipStore {
434
425
  return chip.kind === "url" ? chip.url : chip.path;
435
426
  };
436
427
  // Invariant: scan only draft tokens (and resolved reservation tokens), never emitted payloads.
437
- const expanded = text.replace(CHIP_PATTERN, tag => {
428
+ const expanded = replaceCanonicalPromptChips(text, match => {
429
+ const tag = match.text;
438
430
  const entry = this.#pending.get(tag) ?? this.#pending.get(tag.replace("failed-", "screenshot-"));
439
431
  if (entry === undefined)
440
432
  return resolveChip(tag);
@@ -442,7 +434,7 @@ export class PromptChipStore {
442
434
  throw entry.error;
443
435
  if (includeImages && entry.replacement === undefined)
444
436
  throw new ImageAttachmentError("image-pending");
445
- return entry.replacement === undefined ? tag : entry.replacement.replace(CHIP_PATTERN, resolveChip);
437
+ return entry.replacement === undefined ? tag : replaceCanonicalPromptChips(entry.replacement, replacement => resolveChip(replacement.text));
446
438
  });
447
439
  return { text: expanded, images };
448
440
  }
@@ -229,6 +229,8 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
229
229
  viewportTransientTailRowCount(): number;
230
230
  hasActiveSelection(): boolean;
231
231
  setViewportConfig(config: OwnedUiViewportSettings): void;
232
+ setFullscreenCopyOnSelect(enabled: boolean): void;
233
+ showCopyAcknowledgement(message: string): void;
232
234
  resumeViewportFollowing(): void;
233
235
  noteCompletedAssistantMessage(): void;
234
236
  /** A new session binding owns new mounts even when it reuses semantic invocation ids. */
@@ -3,7 +3,7 @@ import { caretCell } from "../../ui/components/line-input.js";
3
3
  import { PromptInput, promptArrow } from "../../ui/components/prompt-input.js";
4
4
  import { composeSubmittedPromptRows, formatSubmittedPromptTime, submittedPromptLayout, } from "../../ui/components/submitted-prompt.js";
5
5
  import { backgroundSgrSpan, heldNativeHyperlinkStyle, hyperlinkSgrSpan, nativeHyperlinkStyle, overlaySpan, } from "../../ui/components/spans.js";
6
- import { progressStatusText } from "../../ui/components/progress-status.js";
6
+ import { progressStatusFrame, progressStatusText } from "../../ui/components/progress-status.js";
7
7
  import { displayWidth, faint, stripAnsi } from "../../ui/components/text.js";
8
8
  import { PINNED_PI_HIDDEN_COMMAND_NAMES, PINNED_PI_WORKFLOW_COMMAND_NAMES } from "../../integrations/pi/engine/workflows.js";
9
9
  import { createPiExtensionUiBridge } from "../../integrations/pi/components/shell-extension-ui.js";
@@ -62,6 +62,8 @@ export class OwnedUiSessionShellRoot {
62
62
  #lastWorkflowStatusId;
63
63
  // Invariant: the notice is dock chrome, never transcript content; the custom viewport alone uses it.
64
64
  #dockNotice;
65
+ #copyAcknowledgement;
66
+ #copyAcknowledgementTimer;
65
67
  #inputSurface;
66
68
  #inputSurfaceCoordination = "editor";
67
69
  #dockInputReuseEnabled;
@@ -106,10 +108,12 @@ export class OwnedUiSessionShellRoot {
106
108
  ...(this.#customViewport ? { getKeybindings: () => this.editor.keybindingConfig() } : {}),
107
109
  });
108
110
  this.resources = createPiShellLoadedResources(startup.resources ?? [], startup.expanded ?? false);
109
- this.#status = createPiShellStatus(view, progressStatusText, handlers);
111
+ this.#status = createPiShellStatus(view, {
112
+ text: (message, mode) => progressStatusText(message, mode === "custom-viewport" ? "…" : "..."),
113
+ frame: progressStatusFrame,
114
+ }, handlers);
110
115
  this.#status.setProgressPresentation(this.#customViewport ? "custom-viewport" : "pinned");
111
116
  this.#footer = createPiShellFooter(this.#viewWithExtensionStatuses(view), cwd, this.#customViewport ? "a1" : "pi", () => this.#footerLevel);
112
- this.#queued = createPiQueuedInputStatus(view.editor.queuedSubmissions, this.#customViewport ? "custom-viewport" : "pinned");
113
117
  this.editor = createPiShellEditor({
114
118
  ...handlers,
115
119
  keybindingProfile: this.#customViewport ? "a1" : "pi",
@@ -183,6 +187,7 @@ export class OwnedUiSessionShellRoot {
183
187
  },
184
188
  ...(handlers.onPromptSuggestionAccepted === undefined ? {} : { onPromptSuggestionAccepted: handlers.onPromptSuggestionAccepted }),
185
189
  });
190
+ this.#queued = createPiQueuedInputStatus(view.editor.queuedSubmissions, this.#customViewport ? "custom-viewport" : "pinned", this.#customViewport ? () => this.editor.keybindingConfig() : undefined);
186
191
  this.#viewportController = new SessionViewportController({
187
192
  enabled: this.#customViewport,
188
193
  editor: this.editor,
@@ -371,13 +376,14 @@ export class OwnedUiSessionShellRoot {
371
376
  }
372
377
  const height = Math.max(0, this.#componentRuntime.getRows());
373
378
  const dock = this.#renderDockLayout(width);
374
- // Invariant: ordinary transcript content uses the full terminal width. The rail is an
375
- // overlay, not a lost wrapping cell. Submitted prompts alone retain the
376
- // intentional final gutter after their right-aligned timestamp.
377
- const documentWidth = width;
379
+ // Invariant: auto/always own one stable final-column gutter. Every scrollable
380
+ // surface ends before it; the dock remains full-width and hidden mode returns it.
381
+ const documentWidth = this.#viewportController.config.scrollbarAppearance === "hidden"
382
+ ? width
383
+ : Math.max(1, width - 1);
378
384
  const document = this.#renderDocumentLayout(documentWidth);
379
- const steeringRows = this.#renderQueued(width);
380
- const statusRows = this.#renderStatus(width);
385
+ const steeringRows = this.#renderQueued(documentWidth);
386
+ const statusRows = this.#renderStatus(documentWidth);
381
387
  const transientSignature = transientRowsSignature(steeringRows, statusRows);
382
388
  const snapshot = this.#visibleViewportSnapshot;
383
389
  const dockInputCandidate = this.#dockInputCandidate;
@@ -494,6 +500,25 @@ export class OwnedUiSessionShellRoot {
494
500
  if (this.#viewportController.setConfig(config))
495
501
  this.#documentLayouts.clear();
496
502
  }
503
+ setFullscreenCopyOnSelect(enabled) {
504
+ this.#viewportController.setCopyOnSelect(enabled);
505
+ }
506
+ showCopyAcknowledgement(message) {
507
+ this.#clearCopyAcknowledgement(false);
508
+ this.#copyAcknowledgement = message;
509
+ const timer = setTimeout(() => {
510
+ if (this.#copyAcknowledgementTimer !== timer)
511
+ return;
512
+ this.#copyAcknowledgementTimer = undefined;
513
+ this.#copyAcknowledgement = undefined;
514
+ this.#invalidateChrome();
515
+ this.#componentRuntime.requestRender();
516
+ }, COPY_ACKNOWLEDGEMENT_DURATION_MS);
517
+ timer.unref?.();
518
+ this.#copyAcknowledgementTimer = timer;
519
+ this.#invalidateChrome();
520
+ this.#componentRuntime.requestRender();
521
+ }
497
522
  resumeViewportFollowing() {
498
523
  this.#viewportController.resumeFollowing();
499
524
  }
@@ -534,18 +559,36 @@ export class OwnedUiSessionShellRoot {
534
559
  const statusRows = this.#customViewport ? this.#status.renderDock(width) : this.#renderStatus(width);
535
560
  const transientRows = [...queued, ...statusRows, ...this.#renderDockNotice(width)];
536
561
  const aboveWidgets = this.#renderWidgets("aboveEditor", width);
562
+ const copyAcknowledgement = this.#renderCopyAcknowledgement(width);
537
563
  const input = this.#inputSurface.render(width);
538
564
  // Invariant: pointer rows describe the body, not the autocomplete block now preceding it.
539
565
  const body = this.usesDefaultInputSurface() ? this.editor.bodyGeometry?.() : undefined;
540
566
  const belowWidgets = this.#renderWidgets("belowEditor", width);
541
567
  const footer = this.#renderFooter(width);
542
- const rowsWithoutTransient = [...aboveWidgets, ...input, ...belowWidgets, ...footer];
568
+ const rowsWithoutTransient = [...aboveWidgets, ...copyAcknowledgement, ...input, ...belowWidgets, ...footer];
543
569
  return {
544
570
  rows: [...transientRows, ...rowsWithoutTransient],
545
- editorOffset: transientRows.length + aboveWidgets.length + (body?.rowOffset ?? 0),
571
+ editorOffset: transientRows.length + aboveWidgets.length + copyAcknowledgement.length + (body?.rowOffset ?? 0),
546
572
  inputRows: body?.rowCount ?? input.length,
547
573
  };
548
574
  }
575
+ #renderCopyAcknowledgement(width) {
576
+ if (this.#copyAcknowledgement === undefined || width <= 0)
577
+ return [];
578
+ const text = piShellTruncateToWidth(this.#copyAcknowledgement, width);
579
+ return [`${" ".repeat(Math.max(0, width - piShellVisibleWidth(text)))}${piTheme().fg("accent", text)}`];
580
+ }
581
+ #clearCopyAcknowledgement(requestRender) {
582
+ if (this.#copyAcknowledgementTimer !== undefined)
583
+ clearTimeout(this.#copyAcknowledgementTimer);
584
+ this.#copyAcknowledgementTimer = undefined;
585
+ if (this.#copyAcknowledgement === undefined)
586
+ return;
587
+ this.#copyAcknowledgement = undefined;
588
+ this.#invalidateChrome();
589
+ if (requestRender)
590
+ this.#componentRuntime.requestRender();
591
+ }
549
592
  #renderDockNotice(width) {
550
593
  const notice = this.#dockNotice;
551
594
  if (notice === undefined)
@@ -651,12 +694,9 @@ export class OwnedUiSessionShellRoot {
651
694
  continue;
652
695
  const promptLike = block !== undefined
653
696
  && (block.kind === "user" || this.#customViewport && isPiPromptStyleCompaction(block));
654
- const blockWidth = this.#customViewport
655
- && promptLike
656
- && this.#viewportController.config.scrollbarAppearance !== "hidden"
657
- && width > 1
658
- ? width - 1
659
- : width;
697
+ // Invariant: the custom viewport has already removed its rail gutter from this width.
698
+ // Prompt-like and ordinary blocks therefore share one right content edge.
699
+ const blockWidth = width;
660
700
  const blockRows = this.#blockRows(id, block, blockWidth);
661
701
  if (promptLike) {
662
702
  // Compatibility: the first natural prompt gets one breathing row at the document top.
@@ -927,6 +967,7 @@ export class OwnedUiSessionShellRoot {
927
967
  }
928
968
  resetWorkflowPresentation() {
929
969
  this.#dockNotice = undefined;
970
+ this.#clearCopyAcknowledgement(false);
930
971
  for (const id of this.#workflowStatusAnchors.keys()) {
931
972
  this.#transcript.get(id)?.dispose?.();
932
973
  this.#transcript.delete(id);
@@ -1035,6 +1076,7 @@ export class OwnedUiSessionShellRoot {
1035
1076
  this.#inputSurface.setFocused?.(focused);
1036
1077
  }
1037
1078
  dispose() {
1079
+ this.#clearCopyAcknowledgement(false);
1038
1080
  this.clearViewportPointerState();
1039
1081
  this.#themeUnsubscribe();
1040
1082
  this.header.dispose?.();
@@ -1186,6 +1228,7 @@ export class OwnedUiSessionShellRoot {
1186
1228
  this.invalidate();
1187
1229
  }
1188
1230
  }
1231
+ const COPY_ACKNOWLEDGEMENT_DURATION_MS = 1_000;
1189
1232
  const SCROLLBAR_CELL_RESET = "\u001b[22;23;24;25;27;28;29;39;54;55m";
1190
1233
  const KITTY_IMAGE_CONTROL = /\u001b_G[\s\S]*?\u001b\\/g;
1191
1234
  const ITERM_IMAGE_CONTROL = /\u001b]1337;File=[^\u0007]*(?:\u0007|\u001b\\)/g;
@@ -61,6 +61,7 @@ export class OwnedUiSessionShell {
61
61
  #responseCopy;
62
62
  // Rationale: only an executor this shell created owns a spare copy helper worth warming and disposing.
63
63
  #copyExecutor;
64
+ #copyIntentSequence = 0;
64
65
  #unbindClipboardWriter;
65
66
  #damageTerminal;
66
67
  #quitOutro;
@@ -235,6 +236,10 @@ export class OwnedUiSessionShell {
235
236
  root: this.root,
236
237
  mode: tuiMode,
237
238
  ...(this.#customViewport ? {
239
+ // Invariant: the owned shell enables and routes mouse reports itself. Pi's
240
+ // enclosing fullscreen renderer must never establish a competing white selection.
241
+ mouse: false,
242
+ consumeUnhandledMouse: true,
238
243
  onOverlayGeometry: surfaces => this.root.setViewportOverlaySurfaces(surfaces),
239
244
  decorateTerminal: (terminal) => {
240
245
  damageTerminal = new DamageAwareTerminalAdapter(terminal, {
@@ -356,7 +361,17 @@ export class OwnedUiSessionShell {
356
361
  }
357
362
  const routed = this.root.handleViewportPreInput(data, true, Date.now(), this.root.usesDefaultInputSurface() && !this.runtime.hasFocusedOverlay());
358
363
  if (routed.copySelection !== undefined) {
359
- void this.#responseCopy?.submit(routed.copySelection, pendingClipboardWrite);
364
+ const snapshot = routed.copySelection;
365
+ const acknowledgement = copyAcknowledgement(snapshot);
366
+ const intent = ++this.#copyIntentSequence;
367
+ void this.#responseCopy?.submit(snapshot, pendingClipboardWrite).then(result => {
368
+ if (this.#disposed || !runtime.active || intent !== this.#copyIntentSequence
369
+ || result.outcome !== "delivered" && result.outcome !== "submitted-unverified")
370
+ return;
371
+ if (acknowledgement.hasNonWhitespace) {
372
+ this.root.showCopyAcknowledgement(`copied ${acknowledgement.characters} chars to clipboard`);
373
+ }
374
+ });
360
375
  }
361
376
  if (!routed.consumed)
362
377
  return routed.data === data ? undefined : { data: routed.data };
@@ -378,6 +393,7 @@ export class OwnedUiSessionShell {
378
393
  this.root.setEditorPaddingX(initialPiSettings.editorPaddingX);
379
394
  this.root.setAutocompleteMaxVisible(initialPiSettings.autocompleteMaxVisible);
380
395
  this.root.setOutputPad(initialPiSettings.outputPad);
396
+ this.root.setFullscreenCopyOnSelect(initialPiSettings.fullscreenCopyOnSelect);
381
397
  this.root.setHideThinkingBlock(initialPiSettings.hideThinkingBlock);
382
398
  this.root.setMermaidRenderingMode(initialPiSettings.mermaidRenderingMode);
383
399
  this.#showImages = initialPiSettings.showImages;
@@ -421,6 +437,11 @@ export class OwnedUiSessionShell {
421
437
  this.#imageWidthCells = value;
422
438
  this.root.setImagePresentation(this.#showImages, this.#imageWidthCells);
423
439
  } },
440
+ ...(this.#customViewport ? { fullscreenCopyOnSelect: { apply: (value) => {
441
+ if (typeof value !== "boolean")
442
+ throw new TypeError("Fullscreen copy-on-select setting is invalid");
443
+ this.root.setFullscreenCopyOnSelect(value);
444
+ } } } : {}),
424
445
  });
425
446
  if (this.#customViewport && promptHistory !== undefined) {
426
447
  this.#promptHistoryImageSidecar = promptHistory.imageSidecar;
@@ -1516,6 +1537,7 @@ export class OwnedUiSessionShell {
1516
1537
  }
1517
1538
  async #dispose() {
1518
1539
  this.#disposed = true;
1540
+ this.#copyIntentSequence += 1;
1519
1541
  // Invariant: the outro frame is what the terminal shows now, before any cleanup writes.
1520
1542
  const outroFrame = this.#captureQuitOutroFrame();
1521
1543
  this.#responseCopy?.dispose();
@@ -1656,6 +1678,7 @@ export class OwnedUiSessionShell {
1656
1678
  this.#streamPresentation.noteImmediatePresentation();
1657
1679
  const view = this.view();
1658
1680
  if (this.backend.sessionGeneration !== this.#sessionGeneration) {
1681
+ this.#copyIntentSequence += 1;
1659
1682
  this.#responseCopy?.reset();
1660
1683
  this.#cancelWaitingImages();
1661
1684
  this.root.resetPendingPastes();
@@ -2072,6 +2095,17 @@ export class OwnedUiSessionShell {
2072
2095
  function isCoalescedStreamBlock(kind) {
2073
2096
  return kind === "assistant" || kind === "thinking" || kind === "tool-call" || kind === "tool-result";
2074
2097
  }
2098
+ function copyAcknowledgement(snapshot) {
2099
+ if (snapshot.literal !== true)
2100
+ return { characters: snapshot.sourceUnits, hasNonWhitespace: true };
2101
+ let characters = Math.max(0, snapshot.rows.length - 1);
2102
+ let hasNonWhitespace = false;
2103
+ for (const row of snapshot.rows) {
2104
+ characters += row.text.length;
2105
+ hasNonWhitespace ||= /\S/u.test(row.text);
2106
+ }
2107
+ return { characters, hasNonWhitespace };
2108
+ }
2075
2109
  export function formatSessionResumeCommand(metadata) {
2076
2110
  const args = [PRODUCT_TEXT.commandName];
2077
2111
  if (!metadata.usesDefaultSessionDir)
@@ -39,6 +39,7 @@ export declare class SessionViewportController {
39
39
  readonly column: number;
40
40
  readonly row: number;
41
41
  } | undefined;
42
+ setCopyOnSelect(enabled: boolean): void;
42
43
  get frame(): TranscriptViewportFrame | null;
43
44
  setEditorPointerFrame(frame: {
44
45
  readonly rowStart: number;
@@ -28,12 +28,14 @@ export class SessionViewportController {
28
28
  #inputSurfacePending = false;
29
29
  #overlays = [];
30
30
  #pointerOwner;
31
- // Invariant: a left-button sequence begun in dock chrome remains owned by the dock.
31
+ // Invariant: a right-click editor gesture remains owned by the dock.
32
32
  #dockPointerSuppressed = false;
33
- // Invariant: a left-button sequence begun in non-selectable transcript tail chrome is held there.
33
+ // Invariant: a gesture begun on an explicit control remains owned by that control.
34
34
  #tailPointerSuppressed = false;
35
35
  // Invariant: editor selection remains true only while its left button is held.
36
36
  #editorPointerSelecting = false;
37
+ // Invariant: no-drag editor presses replay on release; distinct motion promotes frame selection.
38
+ #pendingEditorClick;
37
39
  // Invariant: pointer routing uses the most recently composed editor rows.
38
40
  #editorPointerFrame;
39
41
  #selectionAutoScrollTimer;
@@ -42,6 +44,7 @@ export class SessionViewportController {
42
44
  #hoveredHyperlinkKey;
43
45
  #lastRequestedSelectionRevision = -1;
44
46
  #presentationRevision = 0;
47
+ #copyOnSelect = true;
45
48
  #activityTimer;
46
49
  constructor(options) {
47
50
  this.#enabled = options.enabled;
@@ -77,6 +80,9 @@ export class SessionViewportController {
77
80
  get pointerPosition() {
78
81
  return this.#pointerPosition;
79
82
  }
83
+ setCopyOnSelect(enabled) {
84
+ this.#copyOnSelect = enabled;
85
+ }
80
86
  get frame() {
81
87
  return this.#viewport.frame;
82
88
  }
@@ -112,9 +118,19 @@ export class SessionViewportController {
112
118
  this.#cancelGesture();
113
119
  this.#viewport.setConfig(this.#config);
114
120
  const selectionRevision = this.#viewport.selectionRevision;
121
+ const coveredSurfaces = [
122
+ ...(this.#overlays ?? []),
123
+ ...(this.#inputSurface === undefined ? [] : [this.#inputSurface]),
124
+ ];
125
+ const coveredCells = coveredSurfaces.flatMap(surface => Array.from({ length: Math.max(0, surface.rowEnd - surface.rowStart + 1) }, (_value, index) => ({
126
+ row: surface.rowStart + index,
127
+ columnStart: surface.columnStart,
128
+ columnEnd: surface.columnEnd,
129
+ })));
115
130
  const frame = this.#viewport.compose({
116
131
  ...input,
117
132
  ...(this.#pointerPosition === undefined ? {} : { pointerPosition: this.#pointerPosition }),
133
+ ...(coveredCells.length === 0 ? {} : { coveredCells }),
118
134
  });
119
135
  // Concurrency: if selection changed during composition, this frame truthfully carries
120
136
  // the older revision and exactly one ordinary follow-up render publishes the latest.
@@ -167,6 +183,7 @@ export class SessionViewportController {
167
183
  this.#clearActivityTimer();
168
184
  this.#stopSelectionAutoScroll();
169
185
  this.#editorPointerSelecting = false;
186
+ this.#pendingEditorClick = undefined;
170
187
  this.#editorPointerFrame = undefined;
171
188
  this.#pointerPosition = undefined;
172
189
  this.#hoveredHyperlinkKey = undefined;
@@ -183,6 +200,7 @@ export class SessionViewportController {
183
200
  this.#dockPointerSuppressed = false;
184
201
  this.#tailPointerSuppressed = false;
185
202
  this.#editorPointerSelecting = false;
203
+ this.#pendingEditorClick = undefined;
186
204
  this.#editorPointerFrame = undefined;
187
205
  this.#pointerPosition = undefined;
188
206
  this.#hoveredHyperlinkKey = undefined;
@@ -302,6 +320,7 @@ export class SessionViewportController {
302
320
  let repaint = false;
303
321
  let forceRepaint = false;
304
322
  let activity = false;
323
+ let completedCopy;
305
324
  const routed = routeMouseInput(data, (event, report) => {
306
325
  const hits = frame.hits;
307
326
  const overModal = this.#modalAt(event.column, event.row);
@@ -375,6 +394,10 @@ export class SessionViewportController {
375
394
  return true;
376
395
  if (this.#viewport.selectionActive) {
377
396
  this.#viewport.extendSelection(event.column, event.row, now, false);
397
+ if (this.#pendingEditorClick !== undefined
398
+ && (event.column !== this.#pendingEditorClick.column || event.row !== this.#pendingEditorClick.row)) {
399
+ this.#pendingEditorClick = undefined;
400
+ }
378
401
  this.#updateSelectionAutoScroll(event.column, event.row, hits.viewportHeight);
379
402
  activity = true;
380
403
  return true;
@@ -415,17 +438,20 @@ export class SessionViewportController {
415
438
  return false;
416
439
  this.#dockPointerSuppressed = false;
417
440
  this.#tailPointerSuppressed = false;
441
+ this.#pendingEditorClick = undefined;
418
442
  this.#stopSelectionAutoScroll();
419
443
  if (this.#viewport.clearSelection())
420
444
  repaint = true;
421
445
  if (overBottom) {
422
446
  this.#viewport.scrollToEnd(now);
447
+ this.#tailPointerSuppressed = true;
423
448
  activity = true;
424
449
  repaint = true;
425
450
  return true;
426
451
  }
427
452
  if (overSticky && hits.sticky !== null) {
428
453
  this.#viewport.scrollTo(hits.sticky.target, now);
454
+ this.#tailPointerSuppressed = true;
429
455
  activity = true;
430
456
  repaint = true;
431
457
  return true;
@@ -439,35 +465,19 @@ export class SessionViewportController {
439
465
  }
440
466
  else {
441
467
  this.#viewport.scrollTo(scrollForTrackPage(geometry, trackRow, frame.scrollTop, hits.viewportHeight), now);
468
+ this.#tailPointerSuppressed = true;
442
469
  }
443
470
  activity = true;
444
471
  repaint = true;
445
472
  return true;
446
473
  }
447
- if (editorFrame !== undefined && event.row >= editorFrame.rowStart && event.row <= editorFrame.rowEnd) {
448
- const handled = this.#editor.handlePointer({
449
- kind: "press",
450
- button: event.button,
451
- column: event.column,
452
- row: event.row - editorFrame.rowStart + 1,
453
- });
454
- if (!handled)
455
- this.#dockPointerSuppressed = true;
456
- this.#editorPointerSelecting = handled;
457
- repaint = true;
458
- return true;
459
- }
460
- if (event.row > hits.viewportHeight) {
461
- this.#dockPointerSuppressed = true;
462
- return true;
463
- }
464
- if (event.row >= 1 && event.row <= hits.viewportHeight && event.column <= frame.contentWidth) {
465
- if (!this.#viewport.pressSelection(event.column, event.row, now)) {
466
- // Invariant: empty rows and transient tail rows must never start Pi selection.
467
- this.#tailPointerSuppressed = true;
468
- repaint = true;
469
- return true;
474
+ if (event.row >= 1 && event.row <= frame.rows.length && event.column <= frame.descriptor.width) {
475
+ if (editorFrame !== undefined && event.row >= editorFrame.rowStart && event.row <= editorFrame.rowEnd) {
476
+ this.#pendingEditorClick = { column: event.column, row: event.row };
470
477
  }
478
+ // Invariant: frame selection includes transient and dock rows without changing
479
+ // semantic ownership; no-drag editor presses replay on release.
480
+ this.#viewport.pressSelection(event.column, event.row, now);
471
481
  repaint = true;
472
482
  return true;
473
483
  }
@@ -490,6 +500,17 @@ export class SessionViewportController {
490
500
  }
491
501
  if (this.#viewport.releaseSelection()) {
492
502
  this.#stopSelectionAutoScroll();
503
+ const pendingEditorClick = this.#pendingEditorClick;
504
+ this.#pendingEditorClick = undefined;
505
+ if (pendingEditorClick !== undefined && this.#editorPointerFrame !== undefined) {
506
+ this.#viewport.clearSelection();
507
+ const editorRow = pendingEditorClick.row - this.#editorPointerFrame.rowStart + 1;
508
+ this.#editor.handlePointer({ kind: "press", button: 0, column: pendingEditorClick.column, row: editorRow });
509
+ this.#editor.handlePointer({ kind: "release", button: 0, column: pendingEditorClick.column, row: editorRow });
510
+ }
511
+ else if (this.#copyOnSelect) {
512
+ completedCopy = this.#viewport.captureSelectedText() ?? undefined;
513
+ }
493
514
  // Platform: restore OSC 8 links only after the held-button selection paint has
494
515
  // ended, then overwrite any terminal-cached hover cells immediately.
495
516
  forceRepaint = true;
@@ -519,7 +540,7 @@ export class SessionViewportController {
519
540
  this.#lastRequestedSelectionRevision = this.#viewport.selectionRevision;
520
541
  this.#requestRender(forceRepaint);
521
542
  }
522
- return routed;
543
+ return completedCopy === undefined ? routed : { ...routed, copySelection: completedCopy };
523
544
  }
524
545
  #modalAt(column, row) {
525
546
  const contains = (surface) => column >= surface.columnStart && column <= surface.columnEnd
@@ -534,13 +555,13 @@ export class SessionViewportController {
534
555
  .find(candidate => column - 1 >= candidate.from && column - 1 < candidate.to);
535
556
  return range === undefined ? undefined : JSON.stringify([row, range]);
536
557
  }
537
- #updateSelectionAutoScroll(column, row, viewportHeight) {
538
- const beyondEdge = row <= 1 || row > viewportHeight;
558
+ #updateSelectionAutoScroll(column, row, frameHeight) {
559
+ const beyondEdge = row <= 1 || row >= frameHeight;
539
560
  if (!beyondEdge) {
540
561
  this.#stopSelectionAutoScroll();
541
562
  return;
542
563
  }
543
- this.#selectionAutoScrollPointer = { column, row };
564
+ this.#selectionAutoScrollPointer = { column, row, direction: row <= 1 ? -1 : 1 };
544
565
  if (this.#selectionAutoScrollTimer !== undefined)
545
566
  return;
546
567
  this.#selectionAutoScrollTimer = setTimeout(() => this.#selectionAutoScrollTick(), SELECTION_AUTO_SCROLL_INTERVAL_MS);
@@ -557,7 +578,8 @@ export class SessionViewportController {
557
578
  const rowsPerTick = scrollbarSelectionRows(this.#config.scrollbarSpeed);
558
579
  for (let row = 0; row < rowsPerTick; row += 1) {
559
580
  const previous = this.#viewport.scrollTop;
560
- this.#viewport.extendSelection(pointer.column, pointer.row);
581
+ this.#viewport.scrollBy(pointer.direction);
582
+ this.#viewport.extendSelection(pointer.column, pointer.row, undefined, false);
561
583
  if (this.#viewport.scrollTop === previous)
562
584
  break;
563
585
  }
@@ -2,7 +2,7 @@ import { type InteractiveLaunchIntent, type LaunchProfileId, type SessionSelecti
2
2
  import type { CliCapabilities } from "./capabilities.js";
3
3
  import type { PackageCommandRequest } from "./packages.js";
4
4
  import type { SessionContextRequest } from "./session-context.js";
5
- import { type PackageCliVerb, type PackageSyntaxDiagnostic } from "./package-messages.js";
5
+ import { type PackageCliVerb, type PackageCommandNamespace, type PackageSyntaxDiagnostic } from "./package-messages.js";
6
6
  export type UpdateChannel = "stable" | "next";
7
7
  export interface CliHandlers {
8
8
  readonly launch: (intent: InteractiveLaunchIntent) => Promise<number>;
@@ -25,9 +25,11 @@ export type CliCommand = {
25
25
  } | {
26
26
  readonly kind: "package-help";
27
27
  readonly verb: PackageCliVerb;
28
+ readonly namespace: PackageCommandNamespace;
28
29
  } | {
29
30
  readonly kind: "package-error";
30
31
  readonly diagnostic: PackageSyntaxDiagnostic;
32
+ readonly namespace: PackageCommandNamespace;
31
33
  } | {
32
34
  readonly kind: "launch";
33
35
  readonly profileId: LaunchProfileId;