@timurproko/a1 0.1.8-dev.368 → 0.1.8-dev.402

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 (87) hide show
  1. package/dist/composition/owned-ui.d.ts +2 -0
  2. package/dist/composition/owned-ui.js +28 -11
  3. package/dist/contracts/owned-ui/image-attachments.d.ts +7 -0
  4. package/dist/contracts/owned-ui/image-attachments.js +7 -0
  5. package/dist/foundation/release/dependency-certification.js +65 -7
  6. package/dist/integrations/pi/components/owned-editor-ux.d.ts +8 -6
  7. package/dist/integrations/pi/components/owned-editor-ux.js +115 -26
  8. package/dist/integrations/pi/components/shell-editor-autocomplete.js +4 -0
  9. package/dist/integrations/pi/components/shell-presenters-transcript.js +15 -5
  10. package/dist/integrations/pi/components/shell-shared-facade.d.ts +14 -5
  11. package/dist/integrations/pi/components/tool-image-presentation.d.ts +21 -0
  12. package/dist/integrations/pi/components/tool-image-presentation.js +86 -0
  13. package/dist/integrations/pi/components/tool-result-adapter.d.ts +2 -2
  14. package/dist/integrations/pi/components/transcript-image-resolver.d.ts +7 -0
  15. package/dist/integrations/pi/components/transcript-image-resolver.js +19 -0
  16. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
  17. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +3 -1
  18. package/dist/integrations/pi/components/upstream/components/tool-execution.d.ts +61 -0
  19. package/dist/integrations/pi/components/upstream/components/tool-execution.js +344 -0
  20. package/dist/integrations/pi/engine/adapter.d.ts +4 -1
  21. package/dist/integrations/pi/engine/adapter.js +66 -28
  22. package/dist/integrations/pi/engine/pending-delivery.d.ts +4 -0
  23. package/dist/integrations/pi/engine/pending-delivery.js +15 -6
  24. package/dist/integrations/pi/engine/tool-rendering.js +26 -3
  25. package/dist/integrations/pi/engine/transcript-image-assets.d.ts +11 -0
  26. package/dist/integrations/pi/engine/transcript-image-assets.js +86 -0
  27. package/dist/integrations/pi/session-ui/clipboard-diagnostics.d.ts +16 -0
  28. package/dist/integrations/pi/session-ui/clipboard-diagnostics.js +65 -0
  29. package/dist/integrations/pi/session-ui/editor-hyperlink-budget.d.ts +9 -0
  30. package/dist/integrations/pi/session-ui/editor-hyperlink-budget.js +23 -0
  31. package/dist/integrations/pi/session-ui/index.d.ts +1 -0
  32. package/dist/integrations/pi/session-ui/index.js +1 -0
  33. package/dist/integrations/pi/session-ui/paste-executor.d.ts +10 -0
  34. package/dist/integrations/pi/session-ui/paste-executor.js +232 -0
  35. package/dist/integrations/pi/session-ui/paste-helper.d.ts +1 -0
  36. package/dist/integrations/pi/session-ui/paste-helper.js +173 -0
  37. package/dist/integrations/pi/session-ui/paste-preparation-client.d.ts +16 -0
  38. package/dist/integrations/pi/session-ui/paste-preparation-client.js +159 -0
  39. package/dist/integrations/pi/session-ui/paste-protocol.d.ts +78 -0
  40. package/dist/integrations/pi/session-ui/paste-protocol.js +18 -0
  41. package/dist/integrations/pi/session-ui/paste-text-preparation.d.ts +20 -0
  42. package/dist/integrations/pi/session-ui/paste-text-preparation.js +134 -0
  43. package/dist/integrations/pi/session-ui/paste-text-worker.d.ts +1 -0
  44. package/dist/integrations/pi/session-ui/paste-text-worker.js +12 -0
  45. package/dist/integrations/pi/session-ui/path-chip-presentation.d.ts +7 -0
  46. package/dist/integrations/pi/session-ui/path-chip-presentation.js +24 -0
  47. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +8 -1
  48. package/dist/integrations/pi/session-ui/prompt-chips.js +129 -149
  49. package/dist/integrations/pi/session-ui/response-copy-coordinator.d.ts +27 -0
  50. package/dist/integrations/pi/session-ui/response-copy-coordinator.js +189 -0
  51. package/dist/integrations/pi/session-ui/response-copy-helper.d.ts +1 -0
  52. package/dist/integrations/pi/session-ui/response-copy-helper.js +122 -0
  53. package/dist/integrations/pi/session-ui/response-copy-protocol.d.ts +56 -0
  54. package/dist/integrations/pi/session-ui/response-copy-protocol.js +5 -0
  55. package/dist/integrations/pi/session-ui/response-copy-transport.d.ts +31 -0
  56. package/dist/integrations/pi/session-ui/response-copy-transport.js +187 -0
  57. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +15 -6
  58. package/dist/integrations/pi/session-ui/session-shell-root.js +25 -6
  59. package/dist/integrations/pi/session-ui/session-shell.js +100 -28
  60. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +2 -2
  61. package/dist/integrations/pi/session-ui/session-viewport-controller.js +4 -3
  62. package/dist/integrations/pi/session-ui/system-clipboard.d.ts +2 -2
  63. package/dist/integrations/pi/session-ui/system-clipboard.js +26 -11
  64. package/dist/integrations/pi/tui-runtime/adapter.js +3 -1
  65. package/dist/native/darwin-arm64/manifest.json +1 -1
  66. package/dist/native/linux-x64/manifest.json +1 -1
  67. package/dist/native/win32-x64/manifest.json +2 -2
  68. package/dist/native/win32-x64/process-guardian.exe +0 -0
  69. package/dist/product-identity.d.ts +1 -1
  70. package/dist/product-identity.js +1 -1
  71. package/dist/product-identity.json +1 -0
  72. package/dist/ui/components/index.d.ts +1 -0
  73. package/dist/ui/components/index.js +1 -0
  74. package/dist/ui/components/selection-copy.d.ts +24 -0
  75. package/dist/ui/components/selection-copy.js +39 -0
  76. package/dist/ui/components/spans.d.ts +5 -2
  77. package/dist/ui/components/spans.js +12 -3
  78. package/dist/ui/components/text-selection.js +2 -0
  79. package/dist/ui/components/text.js +1 -1
  80. package/dist/ui/components/transcript-viewport.d.ts +5 -0
  81. package/dist/ui/components/transcript-viewport.js +100 -23
  82. package/dist/ui/components/visible-hyperlinks.js +40 -8
  83. package/docs/architecture/response-copy-delivery.md +83 -0
  84. package/docs/architecture/tool-image-presentation.md +62 -0
  85. package/docs/ci-release-runbook.md +19 -7
  86. package/docs/openspec-archive-automation.md +124 -0
  87. package/package.json +2 -1
@@ -4,7 +4,9 @@ import { PINNED_PI_HIDDEN_COMMAND_NAMES, PINNED_PI_WORKFLOW_COMMAND_NAMES, } fro
4
4
  import { createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellCollapsedChangelog, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellStatus, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, onPiThemeChange, isPiPromptStyleCompaction, piShellVisibleWidth, piShellTruncateToWidth, piTheme, renderPiShellCommandMessage, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, } from "../components/index.js";
5
5
  import { PiTuiRuntimeAdapter, classifyPiTuiInput, } from "../tui-runtime/index.js";
6
6
  import { PromptChipStore } from "./prompt-chips.js";
7
+ import { EditorHyperlinkBudget } from "./editor-hyperlink-budget.js";
7
8
  import { SessionViewportController } from "./session-viewport-controller.js";
9
+ import { canPreparePasteInline } from "./paste-text-preparation.js";
8
10
  /** Composes backend session state into the owned transcript, viewport, editor, and dock presentation. */
9
11
  export class OwnedUiSessionShellRoot {
10
12
  editor;
@@ -24,7 +26,9 @@ export class OwnedUiSessionShellRoot {
24
26
  #queued;
25
27
  #extensionRenderers;
26
28
  #imageAssets;
27
- #promptChips = new PromptChipStore();
29
+ #promptChips;
30
+ #editorHyperlinks = new EditorHyperlinkBudget();
31
+ #pasteFramingId = 0;
28
32
  #customViewport;
29
33
  #submittedPromptComposer;
30
34
  #viewportController;
@@ -64,6 +68,8 @@ export class OwnedUiSessionShellRoot {
64
68
  }, sessionLayout = "pinned", imageAssets) {
65
69
  this.#view = view;
66
70
  this.#customViewport = sessionLayout === "custom-viewport";
71
+ this.#promptChips = new PromptChipStore({ isolated: this.#customViewport,
72
+ ...(handlers.pasteDiagnostics === undefined ? {} : { onEvent: handlers.pasteDiagnostics }) });
67
73
  this.#submittedPromptComposer = this.#customViewport
68
74
  ? {
69
75
  layout: submittedPromptLayout,
@@ -99,13 +105,25 @@ export class OwnedUiSessionShellRoot {
99
105
  return "";
100
106
  }
101
107
  },
102
- ...(this.#customViewport ? { beginClipboardPaste: () => this.#promptChips.beginPaste(this.editor.getText(), signal => handlers.readClipboardContent?.(signal) ?? Promise.resolve(null), error => handlers.onPasteRejected?.(error), () => handlers.requestRender()) } : {}),
108
+ ...(this.#customViewport ? {
109
+ beginClipboardPaste: () => this.#promptChips.beginPaste(this.editor.getText(), handlers.captureClipboardPaste?.() ?? { kind: "provided", read: signal => handlers.readClipboardContent?.(signal) ?? Promise.resolve(null) }, error => handlers.onPasteRejected?.(error), () => handlers.requestRender()),
110
+ onPasteInput: bytes => {
111
+ try {
112
+ handlers.pasteDiagnostics?.({ request: --this.#pasteFramingId, phase: "framing", bytes, atMs: performance.now(), pending: 0, transport: "terminal" });
113
+ }
114
+ catch { /* Invariant: payload-free diagnostics cannot interfere with the input path. */ }
115
+ },
116
+ deferTextPaste: text => !canPreparePasteInline(text),
117
+ beginTextPaste: text => this.#promptChips.beginPaste(this.editor.getText(), { kind: "text", text }, error => handlers.onPasteRejected?.(error)),
118
+ } : {}),
103
119
  editorAtomicRanges: line => this.#promptChips.atomicRanges(line),
104
120
  editorHiddenRanges: line => this.#promptChips.hiddenRanges(line),
105
- decorateEditorRow: (row, width) => {
121
+ decorateEditorRow: (row, width, rowIndex) => {
122
+ if (rowIndex === 0)
123
+ this.#editorHyperlinks.reset();
106
124
  const plain = stripAnsi(row);
107
125
  const ranges = this.#promptChips.hyperlinkRanges(plain);
108
- if (ranges.length === 0)
126
+ if (ranges.length === 0 || !this.#editorHyperlinks.takeCleanup())
109
127
  return row;
110
128
  const linkResetAndTail = `\u001b]8;;\u001b\\\u001b[24m${" ".repeat(Math.max(0, width - piShellVisibleWidth(row)))}`;
111
129
  // Platform: VS15 is zero-column and default-ignorable. It breaks Windows Terminal's
@@ -115,7 +133,7 @@ export class OwnedUiSessionShellRoot {
115
133
  : row;
116
134
  const decorated = this.#viewportController.editorPointerSelecting
117
135
  ? ranges.reduce((result, range) => backgroundSgrSpan(result, piShellVisibleWidth(plain.slice(0, range.start)), piShellVisibleWidth(plain.slice(0, range.end)), "\u001b[4:4m", "\u001b[24m", piShellVisibleWidth), paintRow)
118
- : ranges.reduce((result, range) => hyperlinkSgrSpan(result, piShellVisibleWidth(plain.slice(0, range.start)), piShellVisibleWidth(plain.slice(0, range.end)), range.target, piShellVisibleWidth), row);
136
+ : ranges.filter(range => this.#editorHyperlinks.take(range.target)).reduce((result, range) => hyperlinkSgrSpan(result, piShellVisibleWidth(plain.slice(0, range.start)), piShellVisibleWidth(plain.slice(0, range.end)), range.target, piShellVisibleWidth), row);
119
137
  // Platform: Windows Terminal can retain stale native dotted-link cells when a mutable
120
138
  // prompt replaces a longer URL. Explicit non-link spaces overwrite that tail.
121
139
  return `${decorated}${linkResetAndTail}`;
@@ -611,6 +629,7 @@ export class OwnedUiSessionShellRoot {
611
629
  firstRow,
612
630
  lastRow: Math.max(firstRow, rows.length - 1),
613
631
  sourceRow: pinnedPromptSourceRow(block, blockRows[0], blockWidth),
632
+ prominentSourceRow: blockRows[0],
614
633
  });
615
634
  }
616
635
  }
@@ -1103,7 +1122,7 @@ function nativeTranscriptLinkColor(text, target) {
1103
1122
  function withoutTerminalBackground(text) {
1104
1123
  return text.replace(TERMINAL_BACKGROUND, "");
1105
1124
  }
1106
- /** A pinned timestamp is content now, not secondary transcript metadata. */
1125
+ /** Preserve the established prompt foreground for quiet and hovered pinned timestamps. */
1107
1126
  function pinnedPromptSourceRow(block, sourceRow, width) {
1108
1127
  if (typeof block.payload !== "object" || block.payload === null)
1109
1128
  return sourceRow;
@@ -1,3 +1,6 @@
1
+ import { ResponseCopyCoordinator } from "./response-copy-coordinator.js";
2
+ import { createResponseCopyExecutor, hasAsyncClipboardOutput, responseCopyDestination } from "./response-copy-transport.js";
3
+ import { MAX_COPY_CONTROL_BYTES } from "./response-copy-protocol.js";
1
4
  import { PromptHistoryController } from "./prompt-history-controller.js";
2
5
  import { PRODUCT_TEXT } from "../../../product-identity.js";
3
6
  import { boundedCleanup } from "../../../foundation/terminal-cleanup/index.js";
@@ -37,6 +40,8 @@ export class OwnedUiSessionShell {
37
40
  #disposed = false;
38
41
  #pointerReporting = false;
39
42
  #customViewport;
43
+ #responseCopy;
44
+ #unbindClipboardWriter;
40
45
  #damageTerminal;
41
46
  #streamPresentation;
42
47
  #removeViewportPreInput;
@@ -72,6 +77,52 @@ export class OwnedUiSessionShell {
72
77
  let streamPresentation;
73
78
  let pendingClipboardWrite = Promise.resolve();
74
79
  let promptSuggestionController = null;
80
+ const terminalCopy = options.terminal !== undefined || hasAsyncClipboardOutput();
81
+ this.#responseCopy = this.#customViewport ? new ResponseCopyCoordinator({
82
+ execute: options.responseCopy?.execute ?? createResponseCopyExecutor({
83
+ ...(options.terminal === undefined && options.clipboard === undefined ? {} : { destination: "terminal" }),
84
+ ...(options.clipboard?.writeText === undefined ? {} : { writeText: (text, signal) => options.clipboard.writeText(text, signal) }),
85
+ ...(terminalCopy ? { terminal: { submit: async (control, signal) => {
86
+ if (signal.aborted || this.#disposed || !runtime?.active)
87
+ throw new Error("Copy canceled");
88
+ // Performance: never grow the real terminal's pending buffer with another clipboard payload.
89
+ if (options.terminal === undefined && process.stdout.writableLength + control.length > MAX_COPY_CONTROL_BYTES) {
90
+ throw new Error("Clipboard terminal is busy");
91
+ }
92
+ runtime.writeControl(control);
93
+ } } } : {}),
94
+ }),
95
+ ...(options.responseCopy?.onEvent === undefined ? {} : { onEvent: options.responseCopy.onEvent }),
96
+ onFailure: result => {
97
+ if (this.#disposed || !runtime?.active)
98
+ return;
99
+ this.root.appendWorkflowStatus(result.outcome === "timed-out" ? "Copy timed out; the clipboard did not respond."
100
+ : result.failure === "size" ? "Selection exceeds this clipboard route's size limit."
101
+ : "Could not copy the selection; the clipboard is unavailable.");
102
+ runtime.requestRender();
103
+ },
104
+ }) : null;
105
+ const readClipboard = async (signal) => {
106
+ if (signal.aborted)
107
+ throw new ImageAttachmentError("image-canceled");
108
+ if (options.clipboard === undefined)
109
+ return runImageWorker({ kind: "clipboard" }, signal);
110
+ try {
111
+ const image = await options.clipboard.readImage?.(signal);
112
+ if (image !== null && image !== undefined) {
113
+ const canonical = await runImageWorker({ kind: "canonicalize", source: image }, signal);
114
+ if (canonical !== null)
115
+ return { kind: "image", ...canonical };
116
+ }
117
+ }
118
+ catch (error) {
119
+ if (error instanceof ImageAttachmentError)
120
+ throw error;
121
+ // Compatibility: an unavailable image reader can still provide text.
122
+ }
123
+ const text = await options.clipboard.readText(signal);
124
+ return text === null ? null : { kind: "text", text };
125
+ };
75
126
  this.root = new OwnedUiSessionShellRoot(this.backend.view(), options.cwd, {
76
127
  getColumns: () => runtime?.viewport().columns ?? options.terminal?.columns ?? 80,
77
128
  getRows: () => runtime?.viewport().rows ?? options.terminal?.rows ?? 24,
@@ -107,34 +158,35 @@ export class OwnedUiSessionShell {
107
158
  this.#promptHistory?.synchronize();
108
159
  },
109
160
  onCopyText: text => {
110
- runtime?.writeControl(`\u001b]52;c;${Buffer.from(text, "utf8").toString("base64")}\u0007`);
111
- const write = options.clipboard === undefined
112
- ? writeSystemClipboardText(text)
113
- : options.clipboard.writeText?.(text) ?? Promise.resolve();
114
- pendingClipboardWrite = write.catch(() => { });
161
+ if (this.#responseCopy !== null) {
162
+ void this.#responseCopy.submitText(text);
163
+ return;
164
+ }
165
+ const write = () => {
166
+ if (this.#disposed)
167
+ return Promise.resolve();
168
+ runtime?.writeControl(`\u001b]52;c;${Buffer.from(text, "utf8").toString("base64")}\u0007`);
169
+ return options.clipboard === undefined ? writeSystemClipboardText(text)
170
+ : options.clipboard.writeText?.(text) ?? Promise.resolve();
171
+ };
172
+ // Compatibility: comparison profiles retain their existing clipboard path.
173
+ pendingClipboardWrite = write().catch(() => { });
115
174
  },
116
175
  readClipboardContent: async (signal = new AbortController().signal) => {
117
176
  await pendingClipboardWrite;
118
- if (signal.aborted)
119
- throw new ImageAttachmentError("image-canceled");
120
- if (options.clipboard === undefined)
121
- return runImageWorker({ kind: "clipboard" }, signal);
122
- try {
123
- const image = await options.clipboard.readImage?.(signal);
124
- if (image !== null && image !== undefined) {
125
- const canonical = await runImageWorker({ kind: "canonicalize", source: image }, signal);
126
- if (canonical !== null)
127
- return { kind: "image", ...canonical };
128
- }
129
- }
130
- catch (error) {
131
- if (error instanceof ImageAttachmentError)
132
- throw error;
133
- // Compatibility: an unavailable native image reader can still provide clipboard text.
134
- }
135
- const text = await options.clipboard.readText(signal);
136
- return text === null ? null : { kind: "text", text };
177
+ return readClipboard(signal);
178
+ },
179
+ captureClipboardPaste: () => {
180
+ const before = this.#responseCopy?.capturePasteBarrier() ?? (async () => true);
181
+ if (options.clipboard !== undefined)
182
+ return { kind: "provided", read: readClipboard, before };
183
+ if (responseCopyDestination(process.env) === "terminal")
184
+ return {
185
+ kind: "provided", before, read: async () => { throw new ImageAttachmentError("paste-unavailable"); },
186
+ };
187
+ return { kind: "native", before };
137
188
  },
189
+ ...(options.pasteDiagnostics === undefined ? {} : { pasteDiagnostics: options.pasteDiagnostics }),
138
190
  }, {
139
191
  ...options.startup,
140
192
  resources: options.startup?.resources ?? shellResourceEntries(this.backend),
@@ -261,8 +313,8 @@ export class OwnedUiSessionShell {
261
313
  this.runtime.renderNow();
262
314
  }
263
315
  const routed = this.root.handleViewportPreInput(data, true, Date.now(), this.root.usesDefaultInputSurface() && !this.runtime.hasFocusedOverlay());
264
- if (routed.copyText !== undefined) {
265
- this.runtime.writeControl(`\u001b]52;c;${Buffer.from(routed.copyText, "utf8").toString("base64")}\u0007`);
316
+ if (routed.copySelection !== undefined) {
317
+ void this.#responseCopy?.submit(routed.copySelection, pendingClipboardWrite);
266
318
  }
267
319
  if (!routed.consumed)
268
320
  return routed.data === data ? undefined : { data: routed.data };
@@ -431,6 +483,14 @@ export class OwnedUiSessionShell {
431
483
  if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
432
484
  this.#resolveStopped?.();
433
485
  });
486
+ this.#unbindClipboardWriter = this.#responseCopy === null ? () => { } : this.backend.bindClipboardWriter(async (text) => {
487
+ const result = await this.#responseCopy.submitText(text);
488
+ if (result.outcome === "delivered")
489
+ return true;
490
+ if (result.outcome === "submitted-unverified")
491
+ return false;
492
+ throw new Error(result.outcome === "timed-out" ? "Clipboard delivery timed out" : "Clipboard delivery could not be completed");
493
+ });
434
494
  if (this.backend.view().lifecycle === "stopped")
435
495
  this.#resolveStopped?.();
436
496
  }
@@ -873,10 +933,15 @@ export class OwnedUiSessionShell {
873
933
  this.runtime.requestRender();
874
934
  return;
875
935
  }
876
- void this.backend.copyWorkflowText(text).then(() => {
877
- this.root.appendWorkflowStatus("Copied selected message to clipboard");
936
+ const generation = this.backend.sessionBindingGeneration;
937
+ void this.backend.copyWorkflowText(text).then(acknowledged => {
938
+ if (this.#disposed || generation !== this.backend.sessionBindingGeneration)
939
+ return;
940
+ this.root.appendWorkflowStatus(acknowledged ? "Copied selected message to clipboard" : "Submitted selected message to clipboard");
878
941
  this.runtime.requestRender();
879
942
  }).catch(error => {
943
+ if (this.#disposed || generation !== this.backend.sessionBindingGeneration)
944
+ return;
880
945
  this.root.appendWorkflowResult({ command: "tree", outcome: "failed", message: error instanceof Error ? error.message : String(error) });
881
946
  this.runtime.requestRender();
882
947
  });
@@ -1007,6 +1072,7 @@ export class OwnedUiSessionShell {
1007
1072
  return this.runWorkflow({ command: "quit", argument: "" });
1008
1073
  }
1009
1074
  async runWorkflow(request) {
1075
+ const copyGeneration = request.command === "copy" ? this.backend.sessionBindingGeneration : undefined;
1010
1076
  if (request.command === "login" && request.selection !== undefined) {
1011
1077
  const setup = this.backend.pinnedAmbientAuthentication(request.selection);
1012
1078
  if (setup) {
@@ -1095,6 +1161,9 @@ export class OwnedUiSessionShell {
1095
1161
  this.runtime.requestRender();
1096
1162
  }
1097
1163
  }
1164
+ if (copyGeneration !== undefined && (this.#disposed || copyGeneration !== this.backend.sessionBindingGeneration)) {
1165
+ return workflowAdapterResult({ command: "copy", outcome: "cancelled", message: "" });
1166
+ }
1098
1167
  if (result.outcome === "requires-selection" && request.command === "model") {
1099
1168
  if (result.messages !== undefined) {
1100
1169
  for (const message of result.messages)
@@ -1246,6 +1315,7 @@ export class OwnedUiSessionShell {
1246
1315
  if (this.#disposed)
1247
1316
  return;
1248
1317
  this.#disposed = true;
1318
+ this.#responseCopy?.dispose();
1249
1319
  this.#cancelWaitingImages();
1250
1320
  const failures = [];
1251
1321
  const attempt = (action) => { try {
@@ -1274,6 +1344,7 @@ export class OwnedUiSessionShell {
1274
1344
  fullscreenExitText = this.runtime.mode !== "fullscreen" ? ""
1275
1345
  : exitMode === "resume-hint" ? resumeHint : [exitTranscript, resumeHint].filter(Boolean).join("\n\n");
1276
1346
  });
1347
+ attempt(() => this.#unbindClipboardWriter());
1277
1348
  attempt(() => this.#unbindPiSettings());
1278
1349
  attempt(() => this.#unbindTerminalSettings());
1279
1350
  attempt(() => this.#unbindShutdownSettings());
@@ -1316,6 +1387,7 @@ export class OwnedUiSessionShell {
1316
1387
  this.#streamPresentation.noteImmediatePresentation();
1317
1388
  const view = this.view();
1318
1389
  if (this.backend.sessionGeneration !== this.#sessionGeneration) {
1390
+ this.#responseCopy?.reset();
1319
1391
  this.#cancelWaitingImages();
1320
1392
  this.root.resetPendingPastes();
1321
1393
  this.#promptSuggestions?.invalidate();
@@ -1,6 +1,6 @@
1
1
  import { type PiTuiPointerSurface } from "../tui-runtime/index.js";
2
2
  import type { OwnedUiViewportSettings } from "../../../contracts/owned-ui/index.js";
3
- import { type TranscriptViewportFrame, type TranscriptViewportFrameInput } from "../../../ui/components/index.js";
3
+ import { type SelectionCopySnapshot, type TranscriptViewportFrame, type TranscriptViewportFrameInput } from "../../../ui/components/index.js";
4
4
  import type { PiShellEditorPort } from "../components/index.js";
5
5
  export interface SessionViewportControllerOptions {
6
6
  readonly enabled: boolean;
@@ -13,7 +13,7 @@ export interface SessionViewportControllerOptions {
13
13
  export interface SessionViewportInputResult {
14
14
  readonly data: string;
15
15
  readonly consumed: boolean;
16
- readonly copyText?: string;
16
+ readonly copySelection?: SelectionCopySnapshot;
17
17
  }
18
18
  /**
19
19
  * Owns the stateful interaction policy of the bare-A1 session viewport. The
@@ -249,11 +249,12 @@ export class SessionViewportController {
249
249
  this.#requestRender();
250
250
  }
251
251
  else {
252
- const copyText = this.#viewport.selectedText();
253
- if (copyText !== null && copyText.length > 0) {
252
+ const copySelection = this.#viewport.captureSelectedText();
253
+ if (copySelection !== null) {
254
254
  this.#viewport.clearSelection();
255
+ this.#stopSelectionAutoScroll();
255
256
  this.#requestRender();
256
- return { data: "", consumed: true, copyText };
257
+ return { data: "", consumed: true, copySelection };
257
258
  }
258
259
  }
259
260
  }
@@ -11,10 +11,10 @@ export declare function preloadSystemClipboard(): void;
11
11
  * private clipboard module. Failures are deliberately non-fatal: a denied or
12
12
  * unavailable clipboard simply makes the paste action a no-op.
13
13
  */
14
- export declare function readSystemClipboardContent(signal?: AbortSignal): Promise<PiShellClipboardContent | null>;
14
+ export declare function readSystemClipboardContent(signal?: AbortSignal, strict?: boolean): Promise<PiShellClipboardContent | null>;
15
15
  export declare function readSystemClipboardImage(reader: SystemClipboardImageReader): Promise<{
16
16
  readonly data: string;
17
17
  readonly mimeType: string;
18
18
  } | null>;
19
- export declare function readSystemClipboardText(signal?: AbortSignal): Promise<string | null>;
19
+ export declare function readSystemClipboardText(signal?: AbortSignal, strict?: boolean): Promise<string | null>;
20
20
  export declare function writeSystemClipboardText(text: string): Promise<void>;
@@ -15,13 +15,13 @@ export function preloadSystemClipboard() {
15
15
  * private clipboard module. Failures are deliberately non-fatal: a denied or
16
16
  * unavailable clipboard simply makes the paste action a no-op.
17
17
  */
18
- export async function readSystemClipboardContent(signal) {
18
+ export async function readSystemClipboardContent(signal, strict = false) {
19
19
  await pendingClipboardWrite;
20
20
  const native = await nativeClipboard();
21
21
  if (native !== null) {
22
22
  try {
23
23
  if (process.platform === "win32" && native.availableFormats().some(isFileDropFormat)) {
24
- const files = await readSystemClipboardText(signal);
24
+ const files = await readSystemClipboardText(signal, strict);
25
25
  if (files !== null)
26
26
  return { kind: "text", text: files };
27
27
  }
@@ -35,7 +35,7 @@ export async function readSystemClipboardContent(signal) {
35
35
  // Compatibility: fall through to text and platform readers.
36
36
  }
37
37
  }
38
- const text = await readSystemClipboardText(signal);
38
+ const text = await readSystemClipboardText(signal, strict);
39
39
  return text === null ? null : { kind: "text", text };
40
40
  }
41
41
  export async function readSystemClipboardImage(reader) {
@@ -74,7 +74,7 @@ export async function readSystemClipboardImage(reader) {
74
74
  }
75
75
  return null;
76
76
  }
77
- export async function readSystemClipboardText(signal) {
77
+ export async function readSystemClipboardText(signal, strict = false) {
78
78
  // Concurrency: Ctrl+C/Ctrl+X and Ctrl+V can arrive in adjacent input turns. Serialize the
79
79
  // read behind our native write so paste never observes the previous value.
80
80
  await pendingClipboardWrite;
@@ -85,6 +85,8 @@ export async function readSystemClipboardText(signal) {
85
85
  const text = await native.getText();
86
86
  if (text.length > 0)
87
87
  return text;
88
+ if (strict)
89
+ return null;
88
90
  }
89
91
  catch {
90
92
  // Compatibility: fall through to the platform command.
@@ -97,12 +99,17 @@ export async function readSystemClipboardText(signal) {
97
99
  "$t=Get-Clipboard -Raw -ErrorAction SilentlyContinue",
98
100
  "if ($null -ne $t -and $t.Length -gt 0) {[Console]::Out.Write($t)} else {$f=Get-Clipboard -Format FileDropList -ErrorAction SilentlyContinue; if ($f) {[Console]::Out.Write(($f | ForEach-Object {$_.FullName}) -join [Environment]::NewLine)}}",
99
101
  ].join("; ");
100
- return execClipboard("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], signal);
102
+ return execClipboard("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], signal, strict);
101
103
  }
102
104
  if (process.platform === "darwin")
103
- return execClipboard("pbpaste", [], signal);
104
- return (await execClipboard("wl-paste", ["--no-newline", "--type", "text"], signal))
105
- ?? execClipboard("xclip", ["-selection", "clipboard", "-o"], signal);
105
+ return execClipboard("pbpaste", [], signal, strict);
106
+ try {
107
+ const value = await execClipboard("wl-paste", ["--no-newline", "--type", "text"], signal, strict);
108
+ if (strict || value !== null)
109
+ return value;
110
+ }
111
+ catch { /* Compatibility: X11 may be available when the Wayland command is not. */ }
112
+ return execClipboard("xclip", ["-selection", "clipboard", "-o"], signal, strict);
106
113
  }
107
114
  export function writeSystemClipboardText(text) {
108
115
  const write = async () => {
@@ -128,14 +135,22 @@ function nativeClipboard() {
128
135
  function isFileDropFormat(format) {
129
136
  return /(?:filedrop|hdrop|shell idlist)/iu.test(format);
130
137
  }
131
- function execClipboard(command, args, signal) {
132
- return new Promise(resolve => {
138
+ function execClipboard(command, args, signal, strict = false) {
139
+ if (signal?.aborted)
140
+ return strict ? Promise.reject(new ImageAttachmentError("paste-timeout")) : Promise.resolve(null);
141
+ return new Promise((resolve, reject) => {
133
142
  execFile(command, args, {
134
143
  encoding: "utf8",
135
144
  maxBuffer: MAX_CLIPBOARD_BYTES,
136
145
  timeout: 5_000,
137
146
  windowsHide: true,
147
+ ...(strict ? { killSignal: "SIGKILL" } : {}),
138
148
  ...(signal === undefined ? {} : { signal }),
139
- }, (error, stdout) => resolve(error || stdout.length === 0 ? null : stdout));
149
+ }, (error, stdout) => {
150
+ if (error && strict)
151
+ reject(new ImageAttachmentError("paste-unavailable"));
152
+ else
153
+ resolve(error || stdout.length === 0 ? null : stdout);
154
+ });
140
155
  });
141
156
  }
@@ -34,7 +34,9 @@ class ComponentBridge {
34
34
  this.traceComposition?.("composition-start");
35
35
  try {
36
36
  const lines = [...this.port.render(width)];
37
- const overflow = lines.findIndex(line => visibleWidth(line) > width);
37
+ // Protocol: Kitty APC carries pixels, not columns. Still validate every ordinary cell
38
+ // around the payload instead of exempting the entire image-bearing row from width checks.
39
+ const overflow = lines.findIndex(line => visibleWidth(line.replace(/\u001b_G[^\u001b]*\u001b\\/g, "")) > width);
38
40
  if (overflow >= 0) {
39
41
  throw new RangeError(`Pi TUI component row ${overflow} exceeds available width ${width}`);
40
42
  }
@@ -5,7 +5,7 @@
5
5
  "platform": "darwin",
6
6
  "architecture": "arm64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-13T13:11:55.967Z",
8
+ "builtAt": "2026-09-14T18:48:36.221Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "9db1726bbe3fc2e8292f2ead1e7e9d0fd4d7d0dc9217b372582bf354b0f565dc",
@@ -5,7 +5,7 @@
5
5
  "platform": "linux",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-13T13:12:46.201Z",
8
+ "builtAt": "2026-09-14T18:48:19.618Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "d8cda6b0c7cb36c0cc41e90802aceebf6c02eaebbc08a83d7d963d2e918dbd7a",
@@ -5,10 +5,10 @@
5
5
  "platform": "win32",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-13T13:12:19.326Z",
8
+ "builtAt": "2026-09-14T18:49:01.865Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian.exe",
11
- "sha256": "e6a7be98f08249e90a59466be1c132959a55a80830b000a660724da4d572887a",
11
+ "sha256": "711c72395a5bd9efb4a302df595a7f38ed4cfafb8968426ebb2ed75a39cf1502",
12
12
  "size": 177664
13
13
  },
14
14
  "provenance": {
@@ -1,4 +1,4 @@
1
- declare const ENVIRONMENT_KEYS: readonly ["configDir", "dataDir", "databasePath", "developmentInstanceId", "developmentRoot", "endpoint", "paneId", "probeTrace", "processGuardianPath", "profileHome", "runtimeDir", "startupTrace", "suggestionDiagnostics", "terminalSessionId"];
1
+ declare const ENVIRONMENT_KEYS: readonly ["clipboardDiagnostics", "configDir", "dataDir", "databasePath", "developmentInstanceId", "developmentRoot", "endpoint", "paneId", "probeTrace", "processGuardianPath", "profileHome", "runtimeDir", "startupTrace", "suggestionDiagnostics", "terminalSessionId"];
2
2
  declare const FILESYSTEM_KEYS: readonly ["slug", "windowsDirectory", "unixDirectory", "temporaryPrefix"];
3
3
  declare const STATE_KEYS: readonly ["windowsControlDirectory", "unixControlDirectory", "developmentDirectory", "piAgentProfile", "piVanillaProfile"];
4
4
  declare const ENDPOINT_KEYS: readonly ["windowsPipeStem", "unixSocketFilename", "metadataFilename", "supervisorLogFilename", "databaseFilename"];
@@ -1,6 +1,6 @@
1
1
  import rawIdentity from "./product-identity.json" with { type: "json" };
2
2
  const ENVIRONMENT_KEYS = [
3
- "configDir", "dataDir", "databasePath", "developmentInstanceId", "developmentRoot", "endpoint",
3
+ "clipboardDiagnostics", "configDir", "dataDir", "databasePath", "developmentInstanceId", "developmentRoot", "endpoint",
4
4
  "paneId", "probeTrace", "processGuardianPath", "profileHome", "runtimeDir", "startupTrace", "suggestionDiagnostics", "terminalSessionId",
5
5
  ];
6
6
  const FILESYSTEM_KEYS = ["slug", "windowsDirectory", "unixDirectory", "temporaryPrefix"];
@@ -10,6 +10,7 @@
10
10
  "temporaryPrefix": "a1-"
11
11
  },
12
12
  "environment": {
13
+ "clipboardDiagnostics": "A1_CLIPBOARD_DIAGNOSTICS",
13
14
  "configDir": "A1_CONFIG_DIR",
14
15
  "dataDir": "A1_DATA_DIR",
15
16
  "databasePath": "A1_DATABASE_PATH",
@@ -9,6 +9,7 @@ export * from "./mouse.js";
9
9
  export * from "./pane.js";
10
10
  export * from "./progress-status.js";
11
11
  export * from "./revision.js";
12
+ export * from "./selection-copy.js";
12
13
  export * from "./scrollbar.js";
13
14
  export * from "./shortcuts.js";
14
15
  export * from "./spans.js";
@@ -9,6 +9,7 @@ export * from "./mouse.js";
9
9
  export * from "./pane.js";
10
10
  export * from "./progress-status.js";
11
11
  export * from "./revision.js";
12
+ export * from "./selection-copy.js";
12
13
  export * from "./scrollbar.js";
13
14
  export * from "./shortcuts.js";
14
15
  export * from "./spans.js";
@@ -0,0 +1,24 @@
1
+ import { type OrderedTextSelection, type TextSelectionLineContent } from "./text-selection.js";
2
+ /** Bound selected-source retention independently of history length and encoded payload size. */
3
+ export declare const MAX_COPY_SOURCE_UNITS: number;
4
+ export declare const MAX_COPY_ROWS = 65536;
5
+ export type SelectionCopyPrompt = "first" | "continuation" | undefined;
6
+ export interface SelectionCopyRow {
7
+ readonly text: string;
8
+ readonly prompt?: "first" | "continuation";
9
+ }
10
+ /** Immutable selected rows only; safe to transfer to an isolated text preparer. */
11
+ export interface SelectionCopySnapshot {
12
+ readonly selection: OrderedTextSelection;
13
+ readonly revision: number;
14
+ /** Prompt copy/cut already supplies exact expanded text; do not apply transcript chrome stripping. */
15
+ readonly literal?: boolean;
16
+ readonly rows: readonly SelectionCopyRow[];
17
+ readonly sourceUnits: number;
18
+ readonly captureMs?: number;
19
+ readonly rejected?: "size";
20
+ }
21
+ /** Shares the viewport's semantic chrome exclusions with isolated copy preparation. */
22
+ export declare function selectionCopyLineContent(text: string, prompt: SelectionCopyPrompt): TextSelectionLineContent;
23
+ /** Extracts one snapshot row with the existing grapheme/newline policy; run expensive rows off-thread. */
24
+ export declare function selectionCopyRowText(snapshot: Pick<SelectionCopySnapshot, "selection" | "literal">, row: SelectionCopyRow, index: number): string;
@@ -0,0 +1,39 @@
1
+ import { textSelectionText, usefulTextLineContent } from "./text-selection.js";
2
+ import { stripAnsi } from "./text.js";
3
+ /** Bound selected-source retention independently of history length and encoded payload size. */
4
+ export const MAX_COPY_SOURCE_UNITS = 32 * 1024 * 1024;
5
+ export const MAX_COPY_ROWS = 65_536;
6
+ /** Shares the viewport's semantic chrome exclusions with isolated copy preparation. */
7
+ export function selectionCopyLineContent(text, prompt) {
8
+ const plain = stripAnsi(text);
9
+ if (prompt !== undefined) {
10
+ if (prompt === "first") {
11
+ const timestamp = /\s+\d{2}:\d{2}\s*$/.exec(plain);
12
+ if (timestamp !== null)
13
+ return usefulTextLineContent(plain.slice(0, timestamp.index), 3);
14
+ }
15
+ return usefulTextLineContent(plain, 3);
16
+ }
17
+ const from = plain.trim().length === 0 || (plain.startsWith(" ") && !plain.startsWith(" ")) ? 2 : 1;
18
+ if (plain.trim().length === 0)
19
+ return { from, to: from + 1 };
20
+ return usefulTextLineContent(plain, from);
21
+ }
22
+ /** Extracts one snapshot row with the existing grapheme/newline policy; run expensive rows off-thread. */
23
+ export function selectionCopyRowText(snapshot, row, index) {
24
+ if (snapshot.literal)
25
+ return row.text;
26
+ let plain = stripAnsi(row.text);
27
+ if (row.prompt === "first") {
28
+ const timestamp = /\s+\d{2}:\d{2}\s*$/.exec(plain);
29
+ if (timestamp !== null)
30
+ plain = plain.slice(0, timestamp.index);
31
+ }
32
+ const from = row.prompt !== undefined ? 3 : plain.startsWith(" ") && !plain.startsWith(" ") ? 2 : 1;
33
+ // Performance: trailing chrome is excluded before extraction; full-row copy need not measure every grapheme.
34
+ plain = plain.trimEnd();
35
+ return textSelectionText({
36
+ start: { line: 0, column: index === snapshot.selection.start.line ? snapshot.selection.start.column : 0 },
37
+ end: { line: 0, column: index === snapshot.selection.end.line ? snapshot.selection.end.column : Number.MAX_SAFE_INTEGER },
38
+ }, [plain], () => ({ from }));
39
+ }
@@ -2,9 +2,12 @@
2
2
  * Replaces the visible columns `[from, to)` of `line` with `span`, preserving
3
3
  * the styling before it and re-asserting whatever was still open afterwards, so
4
4
  * the tail keeps the colour it had. A wide character straddling either edge is
5
- * replaced by spaces rather than split.
5
+ * replaced by spaces rather than split. Opt into the replaced cell's boundary
6
+ * style for background-preserving overlays; the span still owns its decoration.
6
7
  */
7
- export declare function overlaySpan(line: string, from: number, to: number, span: string): string;
8
+ export declare function overlaySpan(line: string, from: number, to: number, span: string, options?: {
9
+ readonly inheritStartStyle?: boolean;
10
+ }): string;
8
11
  /** Wraps visible columns `[from,to)` in an explicit OSC 8 hyperlink. */
9
12
  export declare function hyperlinkSgrSpan(line: string, from: number, to: number, target: string, widthOf?: (text: string) => number): string;
10
13
  /** Returns the OSC 8 target painted at a zero-based visible terminal column. */