@timurproko/a1 0.1.8-dev.299 → 0.1.8-dev.332

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 (95) hide show
  1. package/README.md +16 -4
  2. package/bin/guardian.js +4 -3
  3. package/bin/ui.js +4 -2
  4. package/bin/update-recovery.js +3 -1
  5. package/dist/cli/dispatch.js +1 -1
  6. package/dist/composition/owned-ui.js +2 -1
  7. package/dist/features/launch/profile-paths.d.ts +2 -0
  8. package/dist/features/launch/profile-paths.js +11 -0
  9. package/dist/features/owned-ui/project-trust-prompt.js +1 -1
  10. package/dist/features/owned-ui/settings-app.js +1 -1
  11. package/dist/features/prompt-history/service.d.ts +31 -1
  12. package/dist/features/prompt-history/service.js +294 -129
  13. package/dist/features/prompt-history/store.d.ts +2 -1
  14. package/dist/features/prompt-history/store.js +17 -4
  15. package/dist/features/prompt-history/worker.js +6 -3
  16. package/dist/foundation/launch-context/index.d.ts +59 -0
  17. package/dist/foundation/launch-context/index.js +136 -0
  18. package/dist/foundation/launch-guardian/main.js +6 -4
  19. package/dist/foundation/release/bootstrap.d.ts +6 -1
  20. package/dist/foundation/release/bootstrap.js +59 -13
  21. package/dist/foundation/release/cohort-state.d.ts +1 -0
  22. package/dist/foundation/release/cohort-state.js +1 -0
  23. package/dist/foundation/release/dependency-certification-retention.d.ts +7 -0
  24. package/dist/foundation/release/dependency-certification-retention.js +88 -0
  25. package/dist/foundation/release/dependency-certification.d.ts +24 -0
  26. package/dist/foundation/release/dependency-certification.js +208 -0
  27. package/dist/foundation/release/dependency-layer.d.ts +3 -4
  28. package/dist/foundation/release/dependency-layer.js +6 -37
  29. package/dist/foundation/release/release-gc.js +74 -46
  30. package/dist/foundation/release/release-store.d.ts +2 -0
  31. package/dist/foundation/release/release-store.js +11 -3
  32. package/dist/foundation/release/release.d.ts +3 -1
  33. package/dist/foundation/release/release.js +7 -3
  34. package/dist/foundation/release/restart-certification.d.ts +1 -0
  35. package/dist/foundation/release/restart-certification.js +17 -1
  36. package/dist/foundation/release/update-recovery.d.ts +2 -0
  37. package/dist/foundation/release/update-recovery.js +4 -0
  38. package/dist/foundation/release/update.js +1 -1
  39. package/dist/foundation/release/warmup.d.ts +1 -1
  40. package/dist/foundation/release/warmup.js +18 -6
  41. package/dist/foundation/startup/startup-runtime.js +13 -11
  42. package/dist/foundation/supervision/main.js +8 -7
  43. package/dist/integrations/pi/components/owned-editor-ux.d.ts +2 -1
  44. package/dist/integrations/pi/components/owned-editor-ux.js +105 -30
  45. package/dist/integrations/pi/components/shell-editor-autocomplete.js +3 -0
  46. package/dist/integrations/pi/components/shell-presenters-info.d.ts +9 -2
  47. package/dist/integrations/pi/components/shell-presenters-info.js +35 -4
  48. package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +3 -2
  49. package/dist/integrations/pi/components/shell-selectors-dialogs.js +8 -5
  50. package/dist/integrations/pi/components/shell-shared-facade.d.ts +9 -2
  51. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +12 -0
  52. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +25 -2
  53. package/dist/integrations/pi/engine/adapter.d.ts +33 -1
  54. package/dist/integrations/pi/engine/adapter.js +309 -112
  55. package/dist/integrations/pi/engine/changelog.d.ts +2 -0
  56. package/dist/integrations/pi/engine/changelog.js +63 -0
  57. package/dist/integrations/pi/engine/pending-delivery.d.ts +26 -0
  58. package/dist/integrations/pi/engine/pending-delivery.js +149 -0
  59. package/dist/integrations/pi/engine/resources/changelog.json +12 -0
  60. package/dist/integrations/pi/engine/workflow-controllers.d.ts +1 -1
  61. package/dist/integrations/pi/engine/workflows.d.ts +1 -1
  62. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +3 -1
  63. package/dist/integrations/pi/session-ui/prompt-chips.js +74 -37
  64. package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +0 -2
  65. package/dist/integrations/pi/session-ui/prompt-history-controller.js +3 -8
  66. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +26 -1
  67. package/dist/integrations/pi/session-ui/session-shell-root.js +70 -75
  68. package/dist/integrations/pi/session-ui/session-shell.d.ts +1 -1
  69. package/dist/integrations/pi/session-ui/session-shell.js +67 -13
  70. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +7 -2
  71. package/dist/integrations/pi/session-ui/session-viewport-controller.js +25 -20
  72. package/dist/integrations/pi/session-ui/text-paste.d.ts +5 -0
  73. package/dist/integrations/pi/session-ui/text-paste.js +16 -0
  74. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +9 -2
  75. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +87 -43
  76. package/dist/native/darwin-arm64/manifest.json +1 -1
  77. package/dist/native/linux-x64/manifest.json +1 -1
  78. package/dist/native/win32-x64/manifest.json +2 -2
  79. package/dist/native/win32-x64/process-guardian.exe +0 -0
  80. package/dist/product-identity.d.ts +1 -1
  81. package/dist/product-identity.js +2 -35
  82. package/dist/product-identity.json +1 -23
  83. package/dist/ui/components/transcript-viewport.d.ts +8 -0
  84. package/dist/ui/components/transcript-viewport.js +23 -2
  85. package/dist/ui/components/visible-hyperlinks.d.ts +7 -0
  86. package/dist/ui/components/visible-hyperlinks.js +1 -0
  87. package/dist/ui/settings/declarations.js +1 -0
  88. package/dist/ui/settings/sections.js +11 -1
  89. package/docs/architecture/boundaries.md +2 -1
  90. package/docs/architecture/internal-naming.md +91 -0
  91. package/docs/architecture/resource-and-data-policy.md +9 -4
  92. package/docs/architecture/toolchain.md +15 -1
  93. package/docs/features/prompt-history.md +39 -11
  94. package/docs/manual-code-streaming-cleanup.md +88 -0
  95. package/package.json +8 -3
@@ -17,5 +17,5 @@ export declare class PiWorkflowControllerPort implements AgentWorkflowPort {
17
17
  listWorkflows(): Promise<readonly AgentWorkflowDescriptor[]>;
18
18
  executeWorkflow(workflowId: string, input: AgentJsonValue, signal?: AbortSignal): Promise<AgentJsonValue>;
19
19
  }
20
- export declare const PI_BUILTIN_WORKFLOW_ROUTES: readonly ("name" | "share" | "model" | "login" | "logout" | "settings" | "reload" | "compact" | "new" | "resume" | "quit" | "session" | "scoped-models" | "export" | "import" | "copy" | "changelog" | "hotkeys" | "fork" | "clone" | "tree" | "trust" | "debug" | "arminsayshi" | "dementedelves")[];
20
+ export declare const PI_BUILTIN_WORKFLOW_ROUTES: readonly ("name" | "share" | "model" | "login" | "logout" | "settings" | "reload" | "compact" | "new" | "resume" | "quit" | "session" | "tree" | "scoped-models" | "export" | "import" | "copy" | "changelog" | "hotkeys" | "fork" | "clone" | "trust" | "debug" | "arminsayshi" | "dementedelves")[];
21
21
  export declare const PI_WORKFLOW_CAPABILITIES: readonly WorkflowCapability[];
@@ -178,7 +178,7 @@ export interface PiWorkflowHost {
178
178
  readonly stdout: string;
179
179
  readonly stderr: string;
180
180
  }>;
181
- readChangelog(): Promise<string>;
181
+ readChangelog(sinceVersion?: string): Promise<string>;
182
182
  }
183
183
  export interface PiBashWorkflowResult {
184
184
  readonly command: string;
@@ -11,7 +11,7 @@ export interface PreparedPrompt {
11
11
  /** Owns semantic chips and bounded pending paste references; cancels background work on reset or disposal. */
12
12
  export declare class PromptChipStore {
13
13
  #private;
14
- beginPaste(currentText: string, read: (signal: AbortSignal) => Promise<PiShellClipboardContent | null>, onError: (error: unknown) => void): {
14
+ beginPaste(currentText: string, read: (signal: AbortSignal) => Promise<PiShellClipboardContent | null>, onError: (error: unknown) => void, onImage?: () => void): {
15
15
  marker: string;
16
16
  result: Promise<string>;
17
17
  };
@@ -21,6 +21,8 @@ export declare class PromptChipStore {
21
21
  resetPastes(text: string): string;
22
22
  dispose(): Promise<void>;
23
23
  transformPastedContent(content: PiShellClipboardContent, currentText?: string): string;
24
+ /** Hide provisional clipboard identities until the read identifies an actual image. */
25
+ hiddenRanges(line: string): readonly PiShellEditorTextRange[];
24
26
  atomicRanges(line: string): readonly PiShellEditorTextRange[];
25
27
  hyperlinkRanges(text: string): readonly {
26
28
  start: number;
@@ -5,37 +5,45 @@ import { fileURLToPath } from "node:url";
5
5
  import { canonicalizeClipboardImage } from "./clipboard-image.js";
6
6
  import { assertImageEncodedSize, assertPromptImages, ImageAttachmentError } from "../../../contracts/owned-ui/index.js";
7
7
  import { ImagePreparationClient } from "./image-preparation-client.js";
8
- const CHIP_PATTERN = /\[(?:📷 [^\]]+|📁 [^\]]+|📄 [^\]]+|🖼 {1,2}[^\]]+|🔗 [^\]]+)\]/gu;
8
+ import { prepareTextPaste } from "./text-paste.js";
9
+ const CHIP_PATTERN = /\[(?:paste #\d+ (?:\+\d+ lines|\d+ chars)|📷 [^\]]+|📁 [^\]]+|📄 [^\]]+|🖼 {1,2}[^\]]+|🔗 [^\]]+)\]/gu;
9
10
  const IMAGE_EXTENSION = /\.(?:jpe?g|png|webp|gif|bmp|tiff?)$/iu;
10
11
  const URL_PATTERN = /^https?:\/\/[^\s\u0000-\u001f\u007f]+$/iu;
11
12
  const URL_DISPLAY_LENGTH = 40;
12
13
  /** Owns semantic chips and bounded pending paste references; cancels background work on reset or disposal. */
13
14
  export class PromptChipStore {
14
15
  #chips = new Map();
16
+ // Invariant: clearing the editor never recycles a recoverable text chip's identity.
17
+ #textCounter = 0;
15
18
  #pending = new Map();
16
19
  #preparation = new ImagePreparationClient();
17
20
  #stopping = new Set();
18
- beginPaste(currentText, read, onError) {
19
- const marker = `[📷 preparing-${randomBytes(5).toString("hex")}]`;
21
+ beginPaste(currentText, read, onError, onImage = () => { }) {
22
+ const id = randomBytes(5).toString("hex");
23
+ // Invariant: reserve paste identity immediately, but only display a screenshot after identifying an image.
24
+ const marker = `[📷 screenshot-${id}]`;
20
25
  const job = this.#preparation.start(async (signal) => {
21
26
  const content = await read(signal);
22
27
  entry.kind = content?.kind === "image" ? "image" : "text";
23
- if (content?.kind === "image" && this.#imageCount(currentText) >= 8)
24
- throw new ImageAttachmentError("image-count");
28
+ if (content?.kind === "image") {
29
+ onImage();
30
+ if (this.#imageCount(currentText) >= 8)
31
+ throw new ImageAttachmentError("image-count");
32
+ }
25
33
  return content;
26
34
  });
27
35
  const entry = { marker, job, references: 0, kind: "unknown", completion: job.result.then(content => {
28
36
  if (content === null)
29
37
  return "";
30
38
  if (content.kind === "image") {
31
- return this.#addPreparedImage(content);
39
+ return this.#addPreparedImage(content, id);
32
40
  }
33
41
  return this.transformPastedContent(content);
34
42
  }).catch(error => {
35
43
  entry.error = error instanceof ImageAttachmentError ? error : new ImageAttachmentError("image-codec");
36
44
  if (entry.error.code !== "image-canceled" && entry.references === 0)
37
45
  onError(entry.error);
38
- return marker.replace("preparing-", "failed-");
46
+ return marker.replace("screenshot-", "failed-");
39
47
  }).then(replacement => {
40
48
  entry.replacement = replacement;
41
49
  return replacement;
@@ -93,14 +101,23 @@ export class PromptChipStore {
93
101
  entry.job.cancel();
94
102
  return remaining;
95
103
  }
96
- async dispose() { await Promise.all([this.#preparation.dispose(), ...this.#stopping]); }
104
+ async dispose() {
105
+ await Promise.all([this.#preparation.dispose(), ...this.#stopping]);
106
+ this.#chips.clear();
107
+ this.#pending.clear();
108
+ }
97
109
  #imageCount(text) {
98
- const regular = [...this.#chips.values()].filter(chip => chip.kind === "image" && text.includes(chip.tag)).length;
99
- return regular + [...this.#pending.values()].filter(entry => entry.kind !== "text" && (text.includes(entry.marker) || text.includes(entry.marker.replace("preparing-", "failed-")))).length;
110
+ const tags = new Set([...this.#chips.values()].filter(chip => chip.kind === "image" && text.includes(chip.tag)).map(chip => chip.tag));
111
+ for (const entry of this.#pending.values()) {
112
+ if (entry.kind !== "text" && (text.includes(entry.marker) || text.includes(entry.marker.replace("screenshot-", "failed-")))) {
113
+ // Invariant: a ready screenshot can retain its pending label but still occupies only one slot.
114
+ tags.add(entry.replacement ?? entry.marker);
115
+ }
116
+ }
117
+ return tags.size;
100
118
  }
101
- #addPreparedImage(image) {
102
- const suffix = image.mimeType === "image/jpeg" ? "jpg" : image.mimeType.split("/")[1] ?? "png";
103
- const tag = `[📷 screenshot-${randomBytes(5).toString("hex")}${image.transformed ? "-resized" : ""}.${suffix}]`;
119
+ #addPreparedImage(image, id) {
120
+ const tag = `[📷 screenshot-${id}${image.transformed ? "-resized" : ""}]`;
104
121
  const attachment = Object.freeze({ type: "image", data: image.data, mimeType: image.mimeType });
105
122
  assertPromptImages([attachment]);
106
123
  this.#chips.set(tag, { kind: "image", tag, image: attachment });
@@ -114,7 +131,7 @@ export class PromptChipStore {
114
131
  return "";
115
132
  assertPromptImages([...this.prepareSubmission(currentText).images, { type: "image", ...image }]);
116
133
  const id = randomBytes(5).toString("hex");
117
- const tag = `[📷 screenshot-${id}.png]`;
134
+ const tag = `[📷 screenshot-${id}]`;
118
135
  this.#chips.set(tag, {
119
136
  kind: "image",
120
137
  tag,
@@ -131,8 +148,14 @@ export class PromptChipStore {
131
148
  return this.#recordUnique({ kind: "url", tag: `[🔗 ${label}]`, label, url });
132
149
  }
133
150
  const paths = pathsFromClipboard(text);
134
- if (paths.length === 0)
135
- return text;
151
+ if (paths.length === 0) {
152
+ const paste = prepareTextPaste(text);
153
+ if (paste.label === undefined)
154
+ return paste.text;
155
+ const tag = `[paste #${++this.#textCounter} ${paste.label}]`;
156
+ this.#chips.set(tag, Object.freeze({ kind: "text", tag, text: paste.text }));
157
+ return tag;
158
+ }
136
159
  return paths.map(item => {
137
160
  const label = pathLabel(item.fullPath);
138
161
  if (item.kind === "folder") {
@@ -142,8 +165,22 @@ export class PromptChipStore {
142
165
  return this.#recordUnique({ kind: "file", tag: `[${icon} ${label}]`, path: item.fullPath });
143
166
  }).join("");
144
167
  }
168
+ /** Hide provisional clipboard identities until the read identifies an actual image. */
169
+ hiddenRanges(line) {
170
+ const ranges = [];
171
+ for (const entry of this.#pending.values()) {
172
+ if (entry.kind === "image")
173
+ continue;
174
+ let from = 0;
175
+ while ((from = line.indexOf(entry.marker, from)) >= 0) {
176
+ ranges.push({ start: from, end: from + entry.marker.length });
177
+ from += entry.marker.length;
178
+ }
179
+ }
180
+ return ranges.sort((left, right) => left.start - right.start);
181
+ }
145
182
  atomicRanges(line) {
146
- return [...line.matchAll(CHIP_PATTERN)].map(match => ({
183
+ return [...line.matchAll(CHIP_PATTERN)].filter(match => !match[0].startsWith("[paste #") || this.#chips.has(match[0])).map(match => ({
147
184
  start: match.index,
148
185
  end: match.index + match[0].length,
149
186
  }));
@@ -172,41 +209,41 @@ export class PromptChipStore {
172
209
  return this.#replaceResolvable(text, false).text;
173
210
  }
174
211
  prepareHistoryText(text) {
175
- return this.#replaceResolvable(text, false).text.replace(CHIP_PATTERN, tag => this.#chips.get(tag)?.kind === "image" ? "" : tag).trim();
212
+ return this.#replaceResolvable(text, false, true).text.trim();
176
213
  }
177
214
  prepareSubmission(text) {
178
215
  const expanded = this.#replaceResolvable(text, true);
179
216
  return { text: expanded.text, images: expanded.images };
180
217
  }
181
- #replaceResolvable(text, includeImages) {
182
- let expanded = text;
183
- for (const entry of this.#pending.values()) {
184
- if (!expanded.includes(entry.marker) && !expanded.includes(entry.marker.replace("preparing-", "failed-")))
185
- continue;
186
- if (includeImages && entry.error !== undefined)
187
- throw entry.error;
188
- if (includeImages && entry.replacement === undefined)
189
- throw new ImageAttachmentError("image-pending");
190
- if (entry.replacement !== undefined)
191
- expanded = expanded.replaceAll(entry.marker, entry.replacement);
192
- }
218
+ #replaceResolvable(text, includeImages, omitImages = false) {
193
219
  const images = [];
194
220
  const seenImages = new Set();
195
- for (const match of expanded.matchAll(CHIP_PATTERN)) {
196
- const tag = match[0];
221
+ const resolveChip = (tag) => {
197
222
  const chip = this.#chips.get(tag);
198
223
  if (chip === undefined)
199
- continue;
224
+ return tag;
225
+ if (chip.kind === "text")
226
+ return chip.text;
200
227
  if (chip.kind === "image") {
201
228
  if (includeImages && !seenImages.has(tag)) {
202
229
  images.push(chip.image);
203
230
  seenImages.add(tag);
204
231
  }
205
- continue;
232
+ return omitImages ? "" : tag;
206
233
  }
207
- const value = chip.kind === "url" ? chip.url : chip.path;
208
- expanded = expanded.replaceAll(tag, value);
209
- }
234
+ return chip.kind === "url" ? chip.url : chip.path;
235
+ };
236
+ // Invariant: scan only draft tokens (and resolved reservation tokens), never emitted payloads.
237
+ const expanded = text.replace(CHIP_PATTERN, tag => {
238
+ const entry = this.#pending.get(tag) ?? this.#pending.get(tag.replace("failed-", "screenshot-"));
239
+ if (entry === undefined)
240
+ return resolveChip(tag);
241
+ if (includeImages && entry.error !== undefined)
242
+ throw entry.error;
243
+ if (includeImages && entry.replacement === undefined)
244
+ throw new ImageAttachmentError("image-pending");
245
+ return entry.replacement === undefined ? tag : entry.replacement.replace(CHIP_PATTERN, resolveChip);
246
+ });
210
247
  return { text: expanded, images };
211
248
  }
212
249
  #recordUnique(chip) {
@@ -10,7 +10,6 @@ export declare class PromptHistoryController {
10
10
  fallback: readonly string[];
11
11
  active(): boolean;
12
12
  render(): void;
13
- failure(message: string): void;
14
13
  };
15
14
  constructor(options: {
16
15
  editor: PiShellEditorPort;
@@ -19,7 +18,6 @@ export declare class PromptHistoryController {
19
18
  fallback: readonly string[];
20
19
  active(): boolean;
21
20
  render(): void;
22
- failure(message: string): void;
23
21
  });
24
22
  start(): void;
25
23
  capture(text: string, kind: PromptHistoryKind, cwd: string, sessionId: string): void;
@@ -20,11 +20,9 @@ export class PromptHistoryController {
20
20
  this.#snapshot = { revision: 0, limit: options.limit, entries: [] };
21
21
  this.#fallback = boundedTexts([...options.fallback].reverse(), options.limit);
22
22
  this.#unsubscribeSnapshot = this.#subscribeSnapshot();
23
- this.#unsubscribeFailure = options.store.onFailure(code => {
23
+ this.#unsubscribeFailure = options.store.onFailure(() => {
24
24
  if (this.#disposed)
25
25
  this.#closeFailed = true;
26
- else
27
- options.failure(`Prompt history ${code}; current-session recall remains available.`);
28
26
  });
29
27
  this.#unsubscribe = [
30
28
  options.editor.recall.observe(() => {
@@ -57,10 +55,7 @@ export class PromptHistoryController {
57
55
  if (this.#snapshot.entries.some(entry => entry.submissionId === id))
58
56
  this.#local.delete(id);
59
57
  this.synchronize();
60
- }).catch(() => {
61
- if (!this.#disposed)
62
- this.options.failure("Prompt history unavailable; current-session recall remains available.");
63
- });
58
+ }).catch(() => { });
64
59
  }
65
60
  rememberRecovery(text) {
66
61
  if (this.#disposed || !text.trim())
@@ -111,7 +106,7 @@ export class PromptHistoryController {
111
106
  return;
112
107
  this.#snapshot = snapshot;
113
108
  for (const [id, entry] of this.#local) {
114
- if (entry.committed)
109
+ if (entry.committed || snapshot.entries.some(saved => saved.submissionId === id))
115
110
  this.#local.delete(id);
116
111
  }
117
112
  this.synchronize();
@@ -70,7 +70,7 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
70
70
  readonly getRows: () => number;
71
71
  readonly requestRender: (force?: boolean) => void;
72
72
  readonly onViewportFrame?: (frame: TranscriptViewportFrame) => void;
73
- readonly requestHyperlinkCleanup?: () => void;
73
+ readonly requestHyperlinkCleanup?: (rows?: readonly number[]) => void;
74
74
  readonly enableDockInputReuse?: boolean;
75
75
  readonly persistentHistory?: boolean;
76
76
  readonly historyEditor?: import("../components/index.js").HistoryEditorConstructor;
@@ -116,6 +116,31 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
116
116
  applyTranscriptBlock(block: OwnedUiSessionViewModel["transcript"][number]): void;
117
117
  render(width: number): readonly string[];
118
118
  viewportFrameDescriptor(): TranscriptViewportFrameDescriptor | null;
119
+ /** Read-only, payload-free evidence; observing a pending frame must never render or repair it. */
120
+ viewportPresentationEvidence(): {
121
+ candidate: boolean;
122
+ candidateRevision: number | null;
123
+ currentRevision: number;
124
+ composedRevision: number | null;
125
+ currentPointer: {
126
+ readonly column: number;
127
+ readonly row: number;
128
+ } | null;
129
+ composedPointer: {
130
+ readonly column: number;
131
+ readonly row: number;
132
+ } | null;
133
+ frameId: number | null;
134
+ bottom: {
135
+ readonly row: number;
136
+ readonly columnStart: number;
137
+ readonly columnEnd: number;
138
+ } | null;
139
+ followingEnd: boolean;
140
+ scrollTop: number;
141
+ maxScroll: number;
142
+ cause: "detached" | "initial" | "steady" | "dock-input" | "follow-shift" | "geometry-change" | null;
143
+ };
119
144
  /** Visible non-selectable Steering, alignment, and Working rows, for rendering evidence. */
120
145
  viewportTransientTailRowCount(): number;
121
146
  hasActiveSelection(): boolean;
@@ -1,6 +1,6 @@
1
1
  import { PROMPT_GLYPH, backgroundSgrSpan, caretCell, composeSubmittedPromptRows, displayWidth, faint, formatSubmittedPromptTime, heldNativeHyperlinkStyle, hyperlinkSgrSpan, nativeHyperlinkStyle, overlaySpan, progressStatusText, submittedPromptLayout, stripAnsi, } from "../../../ui/components/index.js";
2
2
  import { PINNED_PI_HIDDEN_COMMAND_NAMES, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "../engine/index.js";
3
- 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, piShellVisibleWidth, piTheme, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, } from "../components/index.js";
3
+ 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, piShellVisibleWidth, piTheme, renderPiShellCommandMessage, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, } from "../components/index.js";
4
4
  import { PiTuiRuntimeAdapter, classifyPiTuiInput, } from "../tui-runtime/index.js";
5
5
  import { PromptChipStore } from "./prompt-chips.js";
6
6
  import { SessionViewportController } from "./session-viewport-controller.js";
@@ -45,7 +45,8 @@ export class OwnedUiSessionShellRoot {
45
45
  #inputSurfaceCoordination = "editor";
46
46
  #dockInputReuseEnabled;
47
47
  #dockInputCandidate = false;
48
- #dockInputSnapshot;
48
+ // Invariant: receipt-time eligibility is evidence, never authorization for composition-time reuse.
49
+ #dockInputRevision;
49
50
  #visibleViewportSnapshot;
50
51
  #fullViewportCompositions = 0;
51
52
  #dockOnlyViewportCompositions = 0;
@@ -94,8 +95,9 @@ export class OwnedUiSessionShellRoot {
94
95
  return "";
95
96
  }
96
97
  },
97
- ...(this.#customViewport ? { beginClipboardPaste: () => this.#promptChips.beginPaste(this.editor.getText(), signal => handlers.readClipboardContent?.(signal) ?? Promise.resolve(null), error => handlers.onPasteRejected?.(error)) } : {}),
98
+ ...(this.#customViewport ? { beginClipboardPaste: () => this.#promptChips.beginPaste(this.editor.getText(), signal => handlers.readClipboardContent?.(signal) ?? Promise.resolve(null), error => handlers.onPasteRejected?.(error), () => handlers.requestRender()) } : {}),
98
99
  editorAtomicRanges: line => this.#promptChips.atomicRanges(line),
100
+ editorHiddenRanges: line => this.#promptChips.hiddenRanges(line),
99
101
  decorateEditorRow: (row, width) => {
100
102
  const plain = stripAnsi(row);
101
103
  const ranges = this.#promptChips.hyperlinkRanges(plain);
@@ -136,7 +138,7 @@ export class OwnedUiSessionShellRoot {
136
138
  enabled: this.#customViewport,
137
139
  editor: this.editor,
138
140
  requestRender: force => this.#componentRuntime.requestRender(force),
139
- requestHyperlinkCleanup: () => handlers.requestHyperlinkCleanup?.(),
141
+ requestHyperlinkCleanup: rows => handlers.requestHyperlinkCleanup?.(rows),
140
142
  hasEditorLinks: () => this.#promptChips.hyperlinkRanges(this.editor.getText()).length > 0,
141
143
  });
142
144
  // Performance: stable painter identities let the neutral viewport retain row-level
@@ -256,6 +258,10 @@ export class OwnedUiSessionShellRoot {
256
258
  * walk of the whole transcript.
257
259
  */
258
260
  applyTranscriptBlock(block) {
261
+ const current = this.#blocksById.get(block.id);
262
+ // Invariant: a full-view/final presentation may preempt queued partials. Never revive an older revision.
263
+ if (current !== undefined && (current.revision > block.revision || current.status === "finalized" && block.status === "live"))
264
+ return;
259
265
  this.#blocksById.set(block.id, block);
260
266
  const component = this.#transcript.get(block.id);
261
267
  if (component === undefined) {
@@ -289,13 +295,12 @@ export class OwnedUiSessionShellRoot {
289
295
  const transientSignature = transientRowsSignature(steeringRows, statusRows);
290
296
  const snapshot = this.#visibleViewportSnapshot;
291
297
  const dockInputCandidate = this.#dockInputCandidate;
292
- const dockInputSnapshot = dockInputCandidate ? this.#dockInputSnapshot : undefined;
293
- const dockInputTransientSignature = dockInputCandidate
294
- ? dockInputSnapshot?.transientSignature ?? transientSignature
295
- : transientSignature;
296
- // Invariant: pending Steering and live Working rows share one non-selectable
297
- // viewport tail; only non-working status, widgets, input, and footer stay docked.
298
- const scrollRows = [...document.rows, ...steeringRows, ...statusRows];
298
+ // Concurrency: input may change after a keyboard receipt but before its paint.
299
+ // Capture only the live inputs this composition will actually represent.
300
+ const viewportRevision = this.#viewportController.presentationRevision;
301
+ const selectionRevision = this.#viewportController.selectionRevision;
302
+ const pointerPosition = this.#viewportController.pointerPosition;
303
+ const inputSurface = this.#inputSurface;
299
304
  const dockRows = dock.rows;
300
305
  const selectableDocumentRowCount = document.rows.length;
301
306
  const dockStartRow = height - dockRows.length + 1;
@@ -311,28 +316,27 @@ export class OwnedUiSessionShellRoot {
311
316
  && snapshot !== undefined
312
317
  && snapshot.width === width
313
318
  && snapshot.height === height
314
- && snapshot.documentRows === (dockInputSnapshot?.documentRows ?? scrollRows)
315
- && snapshot.transientSignature === dockInputTransientSignature
316
- && snapshot.promptAnchors === (dockInputSnapshot?.promptAnchors ?? document.promptAnchors)
319
+ && snapshot.documentRows === document.rows
320
+ && snapshot.transientSignature === transientSignature
321
+ && snapshot.promptAnchors === document.promptAnchors
317
322
  && snapshot.dockLength === dockRows.length
318
- && snapshot.inputSurface === (dockInputSnapshot?.inputSurface ?? this.#inputSurface)
319
- && snapshot.viewportRevision === (dockInputSnapshot?.viewportRevision ?? this.#viewportController.presentationRevision)
320
- && snapshot.selectionRevision === (dockInputSnapshot?.selectionRevision ?? this.#viewportController.selectionRevision)
323
+ && snapshot.inputSurface === inputSurface
324
+ && snapshot.viewportRevision === viewportRevision
325
+ && snapshot.selectionRevision === selectionRevision
321
326
  ? this.#viewportController.composeDockOnly(dockRows, width, height)
322
327
  : null;
323
- // Performance: a spinner tick or queue update can race the input-triggered
324
- // render. Recompute the complete transient signature before reusing rows.
325
- if (frame !== null && dockInputTransientSignature !== transientSignature)
326
- frame = null;
327
328
  if (frame === null) {
328
329
  frame = this.#viewportController.compose({
329
- documentRows: scrollRows,
330
+ // Performance: do not copy the complete document on unchanged dock-only input.
331
+ // Steering and Working retain their one non-selectable viewport-tail ownership.
332
+ documentRows: [...document.rows, ...steeringRows, ...statusRows],
330
333
  ...(this.#viewportController.transcriptPointerSelecting
331
334
  ? { paintDocumentRow: heldNativeHyperlinkStyle }
332
335
  : {}),
333
336
  // Invariant: selection and copying stop at the real document tail; Steering,
334
337
  // fitting alignment, and live Working remain transient presentation chrome.
335
338
  selectableDocumentRowCount,
339
+ ...(document.liveTailStartRow === undefined ? {} : { liveTailStartRow: document.liveTailStartRow }),
336
340
  bottomAlignedTailRowCount: statusRows.length,
337
341
  dockRows,
338
342
  promptAnchors: document.promptAnchors,
@@ -350,13 +354,16 @@ export class OwnedUiSessionShellRoot {
350
354
  this.#visibleViewportSnapshot = {
351
355
  width,
352
356
  height,
353
- documentRows: scrollRows,
357
+ documentRows: document.rows,
354
358
  transientSignature,
355
359
  promptAnchors: document.promptAnchors,
356
360
  dockLength: dockRows.length,
357
- inputSurface: this.#inputSurface,
358
- viewportRevision: this.#viewportController.presentationRevision,
359
- selectionRevision: this.#viewportController.selectionRevision,
361
+ inputSurface,
362
+ // Invariant: a callback during composition cannot bless older rows with its
363
+ // newer revision. Its ordinary pending render will publish the newer state.
364
+ viewportRevision,
365
+ selectionRevision: frame.descriptor.selectionRevision,
366
+ pointerPosition,
360
367
  };
361
368
  this.#onViewportFrame?.(frame);
362
369
  return frame.rows;
@@ -364,6 +371,25 @@ export class OwnedUiSessionShellRoot {
364
371
  viewportFrameDescriptor() {
365
372
  return this.#viewportController.frame?.descriptor ?? null;
366
373
  }
374
+ /** Read-only, payload-free evidence; observing a pending frame must never render or repair it. */
375
+ viewportPresentationEvidence() {
376
+ const frame = this.#viewportController.frame;
377
+ const snapshot = this.#visibleViewportSnapshot;
378
+ return {
379
+ candidate: this.#dockInputCandidate,
380
+ candidateRevision: this.#dockInputCandidate ? this.#dockInputRevision ?? null : null,
381
+ currentRevision: this.#viewportController.presentationRevision,
382
+ composedRevision: snapshot?.viewportRevision ?? null,
383
+ currentPointer: this.#viewportController.pointerPosition ?? null,
384
+ composedPointer: snapshot?.pointerPosition ?? null,
385
+ frameId: frame?.descriptor.frameId ?? null,
386
+ bottom: frame?.hits.bottom ?? null,
387
+ followingEnd: frame?.followingEnd ?? true,
388
+ scrollTop: frame?.scrollTop ?? 0,
389
+ maxScroll: frame?.maxScroll ?? 0,
390
+ cause: frame?.descriptor.cause ?? null,
391
+ };
392
+ }
367
393
  /** Visible non-selectable Steering, alignment, and Working rows, for rendering evidence. */
368
394
  viewportTransientTailRowCount() {
369
395
  return this.#viewportController.frame?.hits.transientTail.length ?? 0;
@@ -485,6 +511,10 @@ export class OwnedUiSessionShellRoot {
485
511
  ...resourceRows,
486
512
  ];
487
513
  const promptAnchors = [];
514
+ // Rationale: a block that is still streaming re-renders every row it owns, so the frame
515
+ // reports where its rows begin. Damage-aware painting uses that to tell a live block's
516
+ // own reflow apart from a repaint that reaches settled rows.
517
+ let liveTailStartRow;
488
518
  for (let index = 0; index < this.#transcriptOrder.length; index += 1) {
489
519
  const id = this.#transcriptOrder[index];
490
520
  const block = this.#blocksById.get(id);
@@ -507,6 +537,9 @@ export class OwnedUiSessionShellRoot {
507
537
  rows.push("");
508
538
  }
509
539
  const firstRow = rows.length;
540
+ if (liveTailStartRow === undefined && block !== undefined && block.status !== "finalized") {
541
+ liveTailStartRow = firstRow;
542
+ }
510
543
  rows.push(...blockRows);
511
544
  if (block?.kind === "user" && blockRows[0] !== undefined) {
512
545
  promptAnchors.push({
@@ -538,7 +571,7 @@ export class OwnedUiSessionShellRoot {
538
571
  text: diagnostic.message,
539
572
  payload: {},
540
573
  }, width, this.#cwd)));
541
- const layout = { rows: Object.freeze(rows), promptAnchors: Object.freeze(promptAnchors) };
574
+ const layout = { rows: Object.freeze(rows), promptAnchors: Object.freeze(promptAnchors), liveTailStartRow };
542
575
  if (this.#extensionHeader === null) {
543
576
  this.#documentLayouts.set(width, layout);
544
577
  // Performance: the custom viewport commonly probes full width and reserved-rail width.
@@ -606,7 +639,7 @@ export class OwnedUiSessionShellRoot {
606
639
  }
607
640
  const id = this.#appendAnchoredWorkflowComponent(width => [
608
641
  "",
609
- ...renderPiShellStatusText(this.#workflowStatusMessages.get(id) ?? message, width, this.#outputPad),
642
+ ...renderPiShellStatusText(this.#workflowStatusMessages.get(id) ?? message, width),
610
643
  ]);
611
644
  this.#workflowStatusMessages.set(id, message);
612
645
  this.#lastWorkflowStatusId = id;
@@ -617,10 +650,8 @@ export class OwnedUiSessionShellRoot {
617
650
  return;
618
651
  }
619
652
  this.#lastWorkflowStatusId = undefined;
620
- const prefix = message.kind === "warning" ? "Warning" : message.kind === "error" ? "Error" : undefined;
621
- const color = message.kind === "accent" ? "accent" : message.kind;
622
- const text = prefix === undefined ? message.message : `${prefix}: ${message.message}`;
623
- this.#appendAnchoredWorkflowComponent(() => ["", ` ${piTheme().fg(color, text)}`]);
653
+ const presentation = { kind: message.kind, message: message.message };
654
+ this.#appendAnchoredWorkflowComponent(width => renderPiShellCommandMessage(presentation, width, this.#outputPad));
624
655
  }
625
656
  appendWorkflowResult(result) {
626
657
  if (result.messages !== undefined) {
@@ -642,7 +673,7 @@ export class OwnedUiSessionShellRoot {
642
673
  }
643
674
  if (result.command === "hotkeys" && result.outcome === "completed") {
644
675
  this.#lastWorkflowStatusId = undefined;
645
- const hotkeys = createPiShellHotkeys();
676
+ const hotkeys = createPiShellHotkeys(this.editor.keybindingConfig(), bindings => this.#extensionRenderers.getShortcuts?.(bindings) ?? [], this.#customViewport ? "a1" : "pi");
646
677
  this.#appendAnchoredWorkflowComponent(width => hotkeys.render(width), () => hotkeys.dispose?.());
647
678
  return;
648
679
  }
@@ -661,26 +692,10 @@ export class OwnedUiSessionShellRoot {
661
692
  }
662
693
  if (result.outcome === "completed" && (result.command === "quit" || result.command === "compact"))
663
694
  return;
664
- if (result.command === "new" && result.outcome === "completed") {
665
- this.appendWorkflowMessage({ kind: "accent", message: result.message });
666
- return;
667
- }
668
- if (result.command === "name" && result.outcome === "completed") {
695
+ if (result.outcome === "completed" && (result.command === "new" || result.command === "name" || result.command === "debug")) {
669
696
  this.#lastWorkflowStatusId = undefined;
670
- this.#appendAnchoredWorkflowComponent(() => [
671
- ...(result.detail ? ["", ` ${piTheme().fg("warning", `Warning: ${result.detail}`)}`] : []),
672
- "",
673
- ` ${piTheme().fg("dim", result.message)}`,
674
- ]);
675
- return;
676
- }
677
- if (result.command === "debug" && result.outcome === "completed") {
678
- this.#lastWorkflowStatusId = undefined;
679
- this.#appendAnchoredWorkflowComponent(() => [
680
- "",
681
- ` ${piTheme().fg("accent", result.message)}`,
682
- ...(result.detail ? [` ${piTheme().fg("muted", result.detail)}`] : []),
683
- ]);
697
+ const presentation = { kind: result.command, message: result.message, ...(result.detail === undefined ? {} : { detail: result.detail }) };
698
+ this.#appendAnchoredWorkflowComponent(width => renderPiShellCommandMessage(presentation, width, this.#outputPad));
684
699
  return;
685
700
  }
686
701
  if (result.command === "arminsayshi" && result.outcome === "completed") {
@@ -785,9 +800,7 @@ export class OwnedUiSessionShellRoot {
785
800
  this.appendWorkflowStatus(message);
786
801
  return;
787
802
  }
788
- this.#lastWorkflowStatusId = undefined;
789
- const prefix = type === "warning" ? "Warning" : "Error";
790
- this.#appendAnchoredWorkflowComponent(() => ["", ` ${piTheme().fg(type, `${prefix}: ${message}`)}`]);
803
+ this.appendWorkflowMessage({ kind: type, message });
791
804
  }
792
805
  extensionFooterData() {
793
806
  return {
@@ -829,8 +842,7 @@ export class OwnedUiSessionShellRoot {
829
842
  handleInput(data) {
830
843
  this.#dockInputCandidate = this.#customViewport && this.#dockInputReuseEnabled
831
844
  && classifyPiTuiInput(data, this.#inputSurfaceCoordination) === "safe";
832
- if (this.#dockInputCandidate)
833
- this.#captureDockInputSnapshot();
845
+ this.#dockInputRevision = this.#dockInputCandidate ? this.#viewportController.presentationRevision : undefined;
834
846
  try {
835
847
  this.#inputSurface.handleInput?.(data);
836
848
  }
@@ -839,23 +851,6 @@ export class OwnedUiSessionShellRoot {
839
851
  throw error;
840
852
  }
841
853
  }
842
- #captureDockInputSnapshot() {
843
- const width = Math.max(1, this.#componentRuntime.getColumns());
844
- const steeringRows = this.#renderQueued(width);
845
- const statusRows = this.#renderStatus(width);
846
- const transientSignature = transientRowsSignature(steeringRows, statusRows);
847
- const snapshot = this.#visibleViewportSnapshot;
848
- this.#dockInputSnapshot = snapshot === undefined
849
- ? undefined
850
- : {
851
- documentRows: snapshot.documentRows,
852
- transientSignature,
853
- promptAnchors: snapshot.promptAnchors,
854
- inputSurface: this.#inputSurface,
855
- viewportRevision: this.#viewportController.presentationRevision,
856
- selectionRevision: this.#viewportController.selectionRevision,
857
- };
858
- }
859
854
  invalidate() {
860
855
  this.header.invalidate();
861
856
  this.resources.invalidate();
@@ -884,7 +879,7 @@ export class OwnedUiSessionShellRoot {
884
879
  this.#documentLayouts.clear();
885
880
  this.#visibleViewportSnapshot = undefined;
886
881
  this.#dockInputCandidate = false;
887
- this.#dockInputSnapshot = undefined;
882
+ this.#dockInputRevision = undefined;
888
883
  if (this.#inputSurface !== this.editor)
889
884
  this.#inputSurface.dispose?.();
890
885
  this.#extensionHeader?.dispose?.();
@@ -37,7 +37,7 @@ export declare class OwnedUiSessionShell {
37
37
  showLoginMethodSelector(providerReference: string): void;
38
38
  showTreeSelector(initialSelectedId?: string): void;
39
39
  showLoginAuthTypeSelector(): void;
40
- showLoginProviderSelector(authType: "oauth" | "api_key"): void;
40
+ showLoginProviderSelector(authType?: "oauth" | "api_key", initialSearchInput?: string): void;
41
41
  showSessionSelector(): void;
42
42
  showSettingsSelector(): void;
43
43
  shutdown(): Promise<AdapterCommandResult>;