@timurproko/a1 0.1.8-dev.572 → 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 (49) hide show
  1. package/dist/app/session-shell/prompt-chips.js +20 -28
  2. package/dist/app/session-shell/session-shell-root.d.ts +2 -0
  3. package/dist/app/session-shell/session-shell-root.js +54 -14
  4. package/dist/app/session-shell/session-shell.js +30 -2
  5. package/dist/app/session-shell/session-viewport-controller.d.ts +1 -0
  6. package/dist/app/session-shell/session-viewport-controller.js +5 -1
  7. package/dist/contracts/owned-ui/index.d.ts +2 -0
  8. package/dist/contracts/owned-ui/index.js +1 -0
  9. package/dist/contracts/owned-ui/prompt-chips.d.ts +15 -0
  10. package/dist/contracts/owned-ui/prompt-chips.js +59 -0
  11. package/dist/contracts/owned-ui/validation.js +1 -1
  12. package/dist/integrations/pi/components/index.d.ts +1 -0
  13. package/dist/integrations/pi/components/index.js +1 -0
  14. package/dist/integrations/pi/components/modal-frame.d.ts +41 -0
  15. package/dist/integrations/pi/components/modal-frame.js +110 -0
  16. package/dist/integrations/pi/components/models-dialog.js +19 -5
  17. package/dist/integrations/pi/components/shell-selectors-dialogs.js +38 -2
  18. package/dist/integrations/pi/components/skills-dialog.js +3 -3
  19. package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +1 -1
  20. package/dist/integrations/pi/components/submitted-prompt-adapter.js +5 -2
  21. package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +2 -1
  22. package/dist/integrations/pi/components/upstream/components/extension-editor.js +7 -6
  23. package/dist/integrations/pi/components/upstream/components/extension-input.js +9 -8
  24. package/dist/integrations/pi/components/upstream/components/extension-selector.js +10 -9
  25. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
  26. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +3 -3
  27. package/dist/integrations/pi/components/upstream/components/session-selector.js +13 -6
  28. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +7 -6
  29. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +15 -12
  30. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +1 -1
  31. package/dist/integrations/pi/components/upstream/components/tree-selector.js +13 -6
  32. package/dist/integrations/pi/components/upstream/components/trust-selector.js +9 -9
  33. package/dist/integrations/pi/engine/compaction-progress.d.ts +2 -6
  34. package/dist/integrations/pi/engine/compaction-progress.js +10 -10
  35. package/dist/integrations/pi/engine/settings-bridge.js +1 -1
  36. package/dist/integrations/pi/engine/settings-port.js +1 -1
  37. package/dist/integrations/pi/tui-runtime/adapter.js +6 -2
  38. package/dist/integrations/pi/tui-runtime/contracts.d.ts +2 -0
  39. package/dist/integrations/pi/tui-runtime/mouse-report-input.d.ts +6 -0
  40. package/dist/integrations/pi/tui-runtime/mouse-report-input.js +34 -0
  41. package/dist/native/darwin-arm64/manifest.json +1 -1
  42. package/dist/native/linux-x64/manifest.json +1 -1
  43. package/dist/native/win32-x64/manifest.json +2 -2
  44. package/dist/native/win32-x64/process-guardian.exe +0 -0
  45. package/dist/ui/components/dialog-panel.js +10 -6
  46. package/dist/ui/components/mouse.d.ts +2 -2
  47. package/dist/ui/components/mouse.js +2 -2
  48. package/dist/ui/components/transcript-viewport.js +35 -13
  49. package/package.json +1 -1
@@ -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. */
@@ -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;
@@ -374,13 +376,14 @@ export class OwnedUiSessionShellRoot {
374
376
  }
375
377
  const height = Math.max(0, this.#componentRuntime.getRows());
376
378
  const dock = this.#renderDockLayout(width);
377
- // Invariant: ordinary transcript content uses the full terminal width. The rail is an
378
- // overlay, not a lost wrapping cell. Submitted prompts alone retain the
379
- // intentional final gutter after their right-aligned timestamp.
380
- 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);
381
384
  const document = this.#renderDocumentLayout(documentWidth);
382
- const steeringRows = this.#renderQueued(width);
383
- const statusRows = this.#renderStatus(width);
385
+ const steeringRows = this.#renderQueued(documentWidth);
386
+ const statusRows = this.#renderStatus(documentWidth);
384
387
  const transientSignature = transientRowsSignature(steeringRows, statusRows);
385
388
  const snapshot = this.#visibleViewportSnapshot;
386
389
  const dockInputCandidate = this.#dockInputCandidate;
@@ -497,6 +500,25 @@ export class OwnedUiSessionShellRoot {
497
500
  if (this.#viewportController.setConfig(config))
498
501
  this.#documentLayouts.clear();
499
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
+ }
500
522
  resumeViewportFollowing() {
501
523
  this.#viewportController.resumeFollowing();
502
524
  }
@@ -537,18 +559,36 @@ export class OwnedUiSessionShellRoot {
537
559
  const statusRows = this.#customViewport ? this.#status.renderDock(width) : this.#renderStatus(width);
538
560
  const transientRows = [...queued, ...statusRows, ...this.#renderDockNotice(width)];
539
561
  const aboveWidgets = this.#renderWidgets("aboveEditor", width);
562
+ const copyAcknowledgement = this.#renderCopyAcknowledgement(width);
540
563
  const input = this.#inputSurface.render(width);
541
564
  // Invariant: pointer rows describe the body, not the autocomplete block now preceding it.
542
565
  const body = this.usesDefaultInputSurface() ? this.editor.bodyGeometry?.() : undefined;
543
566
  const belowWidgets = this.#renderWidgets("belowEditor", width);
544
567
  const footer = this.#renderFooter(width);
545
- const rowsWithoutTransient = [...aboveWidgets, ...input, ...belowWidgets, ...footer];
568
+ const rowsWithoutTransient = [...aboveWidgets, ...copyAcknowledgement, ...input, ...belowWidgets, ...footer];
546
569
  return {
547
570
  rows: [...transientRows, ...rowsWithoutTransient],
548
- editorOffset: transientRows.length + aboveWidgets.length + (body?.rowOffset ?? 0),
571
+ editorOffset: transientRows.length + aboveWidgets.length + copyAcknowledgement.length + (body?.rowOffset ?? 0),
549
572
  inputRows: body?.rowCount ?? input.length,
550
573
  };
551
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
+ }
552
592
  #renderDockNotice(width) {
553
593
  const notice = this.#dockNotice;
554
594
  if (notice === undefined)
@@ -654,12 +694,9 @@ export class OwnedUiSessionShellRoot {
654
694
  continue;
655
695
  const promptLike = block !== undefined
656
696
  && (block.kind === "user" || this.#customViewport && isPiPromptStyleCompaction(block));
657
- const blockWidth = this.#customViewport
658
- && promptLike
659
- && this.#viewportController.config.scrollbarAppearance !== "hidden"
660
- && width > 1
661
- ? width - 1
662
- : 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;
663
700
  const blockRows = this.#blockRows(id, block, blockWidth);
664
701
  if (promptLike) {
665
702
  // Compatibility: the first natural prompt gets one breathing row at the document top.
@@ -930,6 +967,7 @@ export class OwnedUiSessionShellRoot {
930
967
  }
931
968
  resetWorkflowPresentation() {
932
969
  this.#dockNotice = undefined;
970
+ this.#clearCopyAcknowledgement(false);
933
971
  for (const id of this.#workflowStatusAnchors.keys()) {
934
972
  this.#transcript.get(id)?.dispose?.();
935
973
  this.#transcript.delete(id);
@@ -1038,6 +1076,7 @@ export class OwnedUiSessionShellRoot {
1038
1076
  this.#inputSurface.setFocused?.(focused);
1039
1077
  }
1040
1078
  dispose() {
1079
+ this.#clearCopyAcknowledgement(false);
1041
1080
  this.clearViewportPointerState();
1042
1081
  this.#themeUnsubscribe();
1043
1082
  this.header.dispose?.();
@@ -1189,6 +1228,7 @@ export class OwnedUiSessionShellRoot {
1189
1228
  this.invalidate();
1190
1229
  }
1191
1230
  }
1231
+ const COPY_ACKNOWLEDGEMENT_DURATION_MS = 1_000;
1192
1232
  const SCROLLBAR_CELL_RESET = "\u001b[22;23;24;25;27;28;29;39;54;55m";
1193
1233
  const KITTY_IMAGE_CONTROL = /\u001b_G[\s\S]*?\u001b\\/g;
1194
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, {
@@ -357,11 +362,15 @@ export class OwnedUiSessionShell {
357
362
  const routed = this.root.handleViewportPreInput(data, true, Date.now(), this.root.usesDefaultInputSurface() && !this.runtime.hasFocusedOverlay());
358
363
  if (routed.copySelection !== undefined) {
359
364
  const snapshot = routed.copySelection;
365
+ const acknowledgement = copyAcknowledgement(snapshot);
366
+ const intent = ++this.#copyIntentSequence;
360
367
  void this.#responseCopy?.submit(snapshot, pendingClipboardWrite).then(result => {
361
- if (this.#disposed || !runtime.active
368
+ if (this.#disposed || !runtime.active || intent !== this.#copyIntentSequence
362
369
  || result.outcome !== "delivered" && result.outcome !== "submitted-unverified")
363
370
  return;
364
- runtime.showFlash(`Copied ${snapshot.sourceUnits} character${snapshot.sourceUnits === 1 ? "" : "s"} to clipboard`);
371
+ if (acknowledgement.hasNonWhitespace) {
372
+ this.root.showCopyAcknowledgement(`copied ${acknowledgement.characters} chars to clipboard`);
373
+ }
365
374
  });
366
375
  }
367
376
  if (!routed.consumed)
@@ -384,6 +393,7 @@ export class OwnedUiSessionShell {
384
393
  this.root.setEditorPaddingX(initialPiSettings.editorPaddingX);
385
394
  this.root.setAutocompleteMaxVisible(initialPiSettings.autocompleteMaxVisible);
386
395
  this.root.setOutputPad(initialPiSettings.outputPad);
396
+ this.root.setFullscreenCopyOnSelect(initialPiSettings.fullscreenCopyOnSelect);
387
397
  this.root.setHideThinkingBlock(initialPiSettings.hideThinkingBlock);
388
398
  this.root.setMermaidRenderingMode(initialPiSettings.mermaidRenderingMode);
389
399
  this.#showImages = initialPiSettings.showImages;
@@ -427,6 +437,11 @@ export class OwnedUiSessionShell {
427
437
  this.#imageWidthCells = value;
428
438
  this.root.setImagePresentation(this.#showImages, this.#imageWidthCells);
429
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
+ } } } : {}),
430
445
  });
431
446
  if (this.#customViewport && promptHistory !== undefined) {
432
447
  this.#promptHistoryImageSidecar = promptHistory.imageSidecar;
@@ -1522,6 +1537,7 @@ export class OwnedUiSessionShell {
1522
1537
  }
1523
1538
  async #dispose() {
1524
1539
  this.#disposed = true;
1540
+ this.#copyIntentSequence += 1;
1525
1541
  // Invariant: the outro frame is what the terminal shows now, before any cleanup writes.
1526
1542
  const outroFrame = this.#captureQuitOutroFrame();
1527
1543
  this.#responseCopy?.dispose();
@@ -1662,6 +1678,7 @@ export class OwnedUiSessionShell {
1662
1678
  this.#streamPresentation.noteImmediatePresentation();
1663
1679
  const view = this.view();
1664
1680
  if (this.backend.sessionGeneration !== this.#sessionGeneration) {
1681
+ this.#copyIntentSequence += 1;
1665
1682
  this.#responseCopy?.reset();
1666
1683
  this.#cancelWaitingImages();
1667
1684
  this.root.resetPendingPastes();
@@ -2078,6 +2095,17 @@ export class OwnedUiSessionShell {
2078
2095
  function isCoalescedStreamBlock(kind) {
2079
2096
  return kind === "assistant" || kind === "thinking" || kind === "tool-call" || kind === "tool-result";
2080
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
+ }
2081
2109
  export function formatSessionResumeCommand(metadata) {
2082
2110
  const args = [PRODUCT_TEXT.commandName];
2083
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;
@@ -44,6 +44,7 @@ export class SessionViewportController {
44
44
  #hoveredHyperlinkKey;
45
45
  #lastRequestedSelectionRevision = -1;
46
46
  #presentationRevision = 0;
47
+ #copyOnSelect = true;
47
48
  #activityTimer;
48
49
  constructor(options) {
49
50
  this.#enabled = options.enabled;
@@ -79,6 +80,9 @@ export class SessionViewportController {
79
80
  get pointerPosition() {
80
81
  return this.#pointerPosition;
81
82
  }
83
+ setCopyOnSelect(enabled) {
84
+ this.#copyOnSelect = enabled;
85
+ }
82
86
  get frame() {
83
87
  return this.#viewport.frame;
84
88
  }
@@ -504,7 +508,7 @@ export class SessionViewportController {
504
508
  this.#editor.handlePointer({ kind: "press", button: 0, column: pendingEditorClick.column, row: editorRow });
505
509
  this.#editor.handlePointer({ kind: "release", button: 0, column: pendingEditorClick.column, row: editorRow });
506
510
  }
507
- else {
511
+ else if (this.#copyOnSelect) {
508
512
  completedCopy = this.#viewport.captureSelectedText() ?? undefined;
509
513
  }
510
514
  // Platform: restore OSC 8 links only after the held-button selection paint has
@@ -5,6 +5,8 @@ export type { OwnedUiBlockStatus, OwnedUiCommand, OwnedUiCommandOutcome, OwnedUi
5
5
  export { acceptsTranscriptUpdate, transcriptToolState } from "./transcript-lifecycle.js";
6
6
  export { ImageAttachmentError, MAX_CLIPBOARD_TEXT_BYTES, MAX_IMAGE_DATA_BYTES, MAX_PROMPT_IMAGES, assertImageEncodedSize, assertPromptImages, } from "./image-attachments.js";
7
7
  export { CONTEXTUAL_PROMPT_SUGGESTION_INSTRUCTION, normalizePromptSuggestionCandidate } from "./prompt-suggestions.js";
8
+ export { canonicalPromptChipMatches, protectPromptChipWrapping, replaceCanonicalPromptChips } from "./prompt-chips.js";
9
+ export type { PromptChipTextMatch, PromptChipWrapProtection } from "./prompt-chips.js";
8
10
  export { SUGGESTION_DECISION_REASONS, SUGGESTION_DIAGNOSTIC_EVENTS } from "./suggestion-diagnostics.js";
9
11
  export type { SuggestionDecision, SuggestionDecisionReason, SuggestionDiagnosticEvent, SuggestionDiagnosticObserver, SuggestionDiagnosticRecord, } from "./suggestion-diagnostics.js";
10
12
  export { PROMPT_HISTORY_EDITOR_REPLACEMENT, PROMPT_HISTORY_MAX_ENTRY_BYTES, PROMPT_HISTORY_MAX_PENDING, PROMPT_HISTORY_MAX_TEXT_BYTES, assertPromptHistorySubmission, } from "./prompt-history.js";
@@ -3,6 +3,7 @@ export { OWNED_UI_CONTRACT_VERSION } from "./model.js";
3
3
  export { acceptsTranscriptUpdate, transcriptToolState } from "./transcript-lifecycle.js";
4
4
  export { ImageAttachmentError, MAX_CLIPBOARD_TEXT_BYTES, MAX_IMAGE_DATA_BYTES, MAX_PROMPT_IMAGES, assertImageEncodedSize, assertPromptImages, } from "./image-attachments.js";
5
5
  export { CONTEXTUAL_PROMPT_SUGGESTION_INSTRUCTION, normalizePromptSuggestionCandidate } from "./prompt-suggestions.js";
6
+ export { canonicalPromptChipMatches, protectPromptChipWrapping, replaceCanonicalPromptChips } from "./prompt-chips.js";
6
7
  export { SUGGESTION_DECISION_REASONS, SUGGESTION_DIAGNOSTIC_EVENTS } from "./suggestion-diagnostics.js";
7
8
  export { PROMPT_HISTORY_EDITOR_REPLACEMENT, PROMPT_HISTORY_MAX_ENTRY_BYTES, PROMPT_HISTORY_MAX_PENDING, PROMPT_HISTORY_MAX_TEXT_BYTES, assertPromptHistorySubmission, } from "./prompt-history.js";
8
9
  export { assertOwnedUiCommand, assertOwnedUiCustomization, assertOwnedUiDiagnostics, assertOwnedUiEditorState, assertOwnedUiEvent, assertOwnedUiPromptSuggestionIdentity, assertOwnedUiPromptSuggestionRequest, assertOwnedUiPromptSuggestionResult, assertOwnedUiPromptSuggestionState, assertOwnedUiSessionViewModel, assertOwnedUiSnapshot, assertOwnedUiStatusView, assertOwnedUiTerminalSurface, assertOwnedUiTranscriptBlock, } from "./validation.js";
@@ -0,0 +1,15 @@
1
+ export interface PromptChipTextMatch {
2
+ readonly text: string;
3
+ readonly start: number;
4
+ readonly end: number;
5
+ }
6
+ export interface PromptChipWrapProtection {
7
+ readonly text: string;
8
+ restore(rendered: string): string;
9
+ }
10
+ /** Finds canonical prompt-chip labels without requiring their process-local backing values. */
11
+ export declare function canonicalPromptChipMatches(text: string): readonly PromptChipTextMatch[];
12
+ /** Replaces only canonical prompt-chip labels while preserving every intervening source byte. */
13
+ export declare function replaceCanonicalPromptChips(text: string, replacement: (match: PromptChipTextMatch) => string): string;
14
+ /** Protects fitting chip labels for Markdown wrapping and restores their exact visible source text. */
15
+ export declare function protectPromptChipWrapping(text: string): PromptChipWrapProtection;
@@ -0,0 +1,59 @@
1
+ const PROMPT_CHIP_PATTERN = /\[(?:paste #\d+ (?:\+\d+ lines|\d+ chars)|📷 [^\]]+|📁 [^\]]+|📄 [^\]]+|🖼 {1,2}[^\]]+|🔗 [^\]]+)\]/gu;
2
+ const ZERO_WIDTH_MARKERS = ["\u2060", "\u200b", "\u200c", "\u200d", "\ufeff"];
3
+ /** Finds canonical prompt-chip labels without requiring their process-local backing values. */
4
+ export function canonicalPromptChipMatches(text) {
5
+ return [...text.matchAll(PROMPT_CHIP_PATTERN)].map(match => ({
6
+ text: match[0], start: match.index, end: match.index + match[0].length,
7
+ }));
8
+ }
9
+ /** Replaces only canonical prompt-chip labels while preserving every intervening source byte. */
10
+ export function replaceCanonicalPromptChips(text, replacement) {
11
+ return text.replace(PROMPT_CHIP_PATTERN, (value, offset) => replacement({
12
+ text: value, start: offset, end: offset + value.length,
13
+ }));
14
+ }
15
+ /** Protects fitting chip labels for Markdown wrapping and restores their exact visible source text. */
16
+ export function protectPromptChipWrapping(text) {
17
+ const matches = canonicalPromptChipMatches(text);
18
+ if (matches.length === 0)
19
+ return { text, restore: identity };
20
+ const spaceMarker = unusedPrivateMarker(text);
21
+ const boundaryMarker = unusedZeroWidthMarker(text);
22
+ let protectedText = "", cursor = 0;
23
+ for (const match of matches) {
24
+ protectedText += text.slice(cursor, match.start);
25
+ // Rationale: adjacent chips need a temporary break opportunity or Markdown treats the complete run as one word.
26
+ if (match.start === cursor && cursor > 0)
27
+ protectedText += `${boundaryMarker} `;
28
+ protectedText += match.text.replaceAll(" ", spaceMarker);
29
+ cursor = match.end;
30
+ }
31
+ protectedText += text.slice(cursor);
32
+ return {
33
+ text: protectedText,
34
+ restore: rendered => rendered
35
+ .replaceAll(`${boundaryMarker} `, "")
36
+ .replaceAll(boundaryMarker, "")
37
+ .replaceAll(spaceMarker, " "),
38
+ };
39
+ }
40
+ function unusedPrivateMarker(text) {
41
+ for (let codePoint = 0xe000; codePoint <= 0xf8ff; codePoint++) {
42
+ const marker = String.fromCodePoint(codePoint);
43
+ if (!text.includes(marker))
44
+ return marker;
45
+ }
46
+ throw new Error("Prompt chip text exhausts the private presentation marker range");
47
+ }
48
+ function unusedZeroWidthMarker(text) {
49
+ for (const marker of ZERO_WIDTH_MARKERS)
50
+ if (!text.includes(marker))
51
+ return marker;
52
+ for (const left of ZERO_WIDTH_MARKERS) {
53
+ for (const right of ZERO_WIDTH_MARKERS)
54
+ if (!text.includes(left + right))
55
+ return left + right;
56
+ }
57
+ throw new Error("Prompt chip text exhausts the zero-width presentation marker range");
58
+ }
59
+ function identity(value) { return value; }
@@ -353,7 +353,7 @@ export function assertOwnedUiStatusView(status) {
353
353
  assertBoundedText(status.title, "owned-UI status title", MAX_LABEL_LENGTH);
354
354
  assertOptionalText(status.workingMessage, "owned-UI working message", MAX_MESSAGE_LENGTH);
355
355
  if (status.workingProgress !== undefined && status.workingProgress !== null) {
356
- assertIntegerInRange(status.workingProgress, 0, 99, "owned-UI working progress");
356
+ assertIntegerInRange(status.workingProgress, 0, 100, "owned-UI working progress");
357
357
  }
358
358
  assertCollection(status.diagnostics, "owned-UI status diagnostics", MAX_STATUS_DIAGNOSTICS);
359
359
  for (const diagnostic of status.diagnostics) {
@@ -10,6 +10,7 @@ export { PINNED_PI_LAYOUT, applyConfiguredPiTheme, applyPiTheme, applyPiThemeIns
10
10
  export type { PiColorMode, PiTerminalTheme, PiTerminalThemeDetection, PiTerminalThemeDetector, PiThemeBackground, PiThemeResult } from "./theme.js";
11
11
  export { OwnedPiThemeController } from "./upstream/theme/theme-controller.js";
12
12
  export type { PiThemeRuntimePort, PiThemeSettingsPort } from "./upstream/theme/theme-controller.js";
13
+ export { PI_MODAL_CONTENT_PADDING_X, PiModalFrame, PiModalHeader, addPiModalHeader, adoptPiModalFrame, adoptPiModalHeader, } from "./modal-frame.js";
13
14
  export { ModelsDialogComponent } from "./models-dialog.js";
14
15
  export type { ModelsDialogCallbacks, ModelsDialogConfig, ModelsDialogFilter, ModelsDialogModel } from "./models-dialog.js";
15
16
  export { CountdownTimer } from "./upstream/components/countdown-timer.js";
@@ -5,6 +5,7 @@ export { OWNED_BUILTIN_SLASH_COMMANDS, PINNED_PI_BUILTIN_SLASH_COMMANDS, compone
5
5
  export { renderPiShellHotkeySections } from "./shell-hotkey-sections.js";
6
6
  export { PINNED_PI_LAYOUT, applyConfiguredPiTheme, applyPiTheme, applyPiThemeInstance, currentPiThemeName, detectPiTerminalBackgroundFromEnv, detectPiTerminalBackgroundTheme, detectPiTerminalThemeForAuto, ensurePiTheme, getAvailablePiThemes, loadPiTheme, onPiThemeChange, parsePiAutoThemeSetting, piTheme, resolvePiThemeSetting, stopPiThemeWatcher, } from "./theme.js";
7
7
  export { OwnedPiThemeController } from "./upstream/theme/theme-controller.js";
8
+ export { PI_MODAL_CONTENT_PADDING_X, PiModalFrame, PiModalHeader, addPiModalHeader, adoptPiModalFrame, adoptPiModalHeader, } from "./modal-frame.js";
8
9
  export { ModelsDialogComponent } from "./models-dialog.js";
9
10
  export { CountdownTimer } from "./upstream/components/countdown-timer.js";
10
11
  export { ExtensionEditorComponent } from "./upstream/components/extension-editor.js";
@@ -0,0 +1,41 @@
1
+ import { Container, type Component } from "@earendil-works/pi-tui";
2
+ export declare const PI_MODAL_CONTENT_PADDING_X = 1;
3
+ /** The semantic top rule/title pair used while a modal assembles its frame. */
4
+ export declare class PiModalHeader implements Component {
5
+ readonly rule: Component;
6
+ readonly title: Component;
7
+ constructor(rule: Component, title: Component);
8
+ invalidate(): void;
9
+ render(width: number): string[];
10
+ }
11
+ /**
12
+ * Complete framed-modal chrome. Rules remain frame-wide; every semantic content component renders
13
+ * in the same one-cell-smaller rectangle and receives the same left inset.
14
+ */
15
+ export declare class PiModalFrame implements Component {
16
+ #private;
17
+ constructor(topRule: Component, content: readonly Component[], bottomRule: Component, fullWidthContent?: readonly Component[], preInsetContent?: readonly Component[]);
18
+ invalidate(): void;
19
+ render(width: number): string[];
20
+ }
21
+ /** Add compact titled-modal chrome while a component assembles its semantic body. */
22
+ export declare function addPiModalHeader(container: Container, rule: Component, title: Component): PiModalHeader;
23
+ export interface PiModalFrameAdoption {
24
+ readonly topIndex: number;
25
+ readonly bottomIndex: number;
26
+ readonly header?: PiModalHeader;
27
+ readonly fullWidthContent?: readonly Component[];
28
+ /** Public pinned children that already own the same one-cell outer inset. */
29
+ readonly preInsetContent?: readonly Component[];
30
+ }
31
+ /**
32
+ * Replace explicitly identified structural modal children with the shared padded frame. Callers
33
+ * identify semantic rules/components; rendered text is never inspected or rewritten.
34
+ */
35
+ export declare function adoptPiModalFrame(container: Container, adoption: PiModalFrameAdoption): PiModalFrame;
36
+ /**
37
+ * Adopt a public component's semantic rule/title children at the bare-A1 boundary. Only an optional
38
+ * structural Spacer between those explicitly identified children is accepted; rendered text is
39
+ * never inspected or rewritten. The pinned comparison profile does not call this adapter.
40
+ */
41
+ export declare function adoptPiModalHeader(container: Container, ruleIndex: number, titleIndex: number): PiModalHeader;