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

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 (110) hide show
  1. package/README.md +40 -11
  2. package/dist/composition/owned-ui.d.ts +2 -0
  3. package/dist/composition/owned-ui.js +20 -3
  4. package/dist/contracts/owned-ui/index.d.ts +2 -0
  5. package/dist/contracts/owned-ui/index.js +2 -0
  6. package/dist/contracts/owned-ui/model.d.ts +38 -3
  7. package/dist/contracts/owned-ui/prompt-history.d.ts +18 -0
  8. package/dist/contracts/owned-ui/prompt-suggestions.d.ts +1 -1
  9. package/dist/contracts/owned-ui/prompt-suggestions.js +4 -1
  10. package/dist/contracts/owned-ui/suggestion-diagnostics.d.ts +22 -0
  11. package/dist/contracts/owned-ui/suggestion-diagnostics.js +9 -0
  12. package/dist/contracts/owned-ui/transcript-lifecycle.d.ts +5 -0
  13. package/dist/contracts/owned-ui/transcript-lifecycle.js +33 -0
  14. package/dist/contracts/owned-ui/validation.js +45 -2
  15. package/dist/contracts/presentation/index.d.ts +8 -0
  16. package/dist/features/owned-ui/settings-app.js +1 -1
  17. package/dist/features/prompt-history/image-sidecar.d.ts +32 -0
  18. package/dist/features/prompt-history/image-sidecar.js +130 -0
  19. package/dist/features/prompt-history/index.d.ts +2 -0
  20. package/dist/features/prompt-history/index.js +1 -0
  21. package/dist/features/prompt-history/paths.d.ts +1 -0
  22. package/dist/features/prompt-history/paths.js +2 -1
  23. package/dist/features/prompt-history/store.d.ts +1 -1
  24. package/dist/features/prompt-history/store.js +46 -2
  25. package/dist/features/prompt-history/worker.js +1 -1
  26. package/dist/features/prompt-suggestions/diagnostics.d.ts +18 -0
  27. package/dist/features/prompt-suggestions/diagnostics.js +82 -0
  28. package/dist/features/prompt-suggestions/index.d.ts +1 -0
  29. package/dist/features/prompt-suggestions/index.js +1 -0
  30. package/dist/foundation/release/bootstrap.d.ts +2 -0
  31. package/dist/foundation/release/bootstrap.js +54 -18
  32. package/dist/foundation/release/index.d.ts +1 -0
  33. package/dist/foundation/release/index.js +1 -0
  34. package/dist/foundation/release/update-launch.d.ts +6 -0
  35. package/dist/foundation/release/update-launch.js +17 -0
  36. package/dist/foundation/release/update.d.ts +2 -0
  37. package/dist/foundation/release/update.js +20 -21
  38. package/dist/foundation/supervision/main.js +5 -2
  39. package/dist/foundation/supervision/server.js +40 -13
  40. package/dist/integrations/pi/components/owned-editor-ux.d.ts +9 -1
  41. package/dist/integrations/pi/components/owned-editor-ux.js +11 -8
  42. package/dist/integrations/pi/components/prompt-input-port.d.ts +18 -0
  43. package/dist/integrations/pi/components/prompt-input-port.js +1 -0
  44. package/dist/integrations/pi/components/shell-editor-autocomplete.js +66 -11
  45. package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
  46. package/dist/integrations/pi/components/shell-footer-status.js +9 -5
  47. package/dist/integrations/pi/components/shell-presenters-info.js +7 -2
  48. package/dist/integrations/pi/components/shell-presenters-transcript.d.ts +6 -2
  49. package/dist/integrations/pi/components/shell-presenters-transcript.js +81 -49
  50. package/dist/integrations/pi/components/shell-shared-facade.d.ts +17 -4
  51. package/dist/integrations/pi/components/shell-shared-facade.js +2 -1
  52. package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +3 -1
  53. package/dist/integrations/pi/components/submitted-prompt-adapter.js +20 -4
  54. package/dist/integrations/pi/components/tool-result-adapter.d.ts +6 -0
  55. package/dist/integrations/pi/components/tool-result-adapter.js +27 -0
  56. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -0
  57. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +5 -0
  58. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +7 -2
  59. package/dist/integrations/pi/components/upstream/components/owned-editor.js +41 -33
  60. package/dist/integrations/pi/components/upstream/components/session-footer.d.ts +2 -1
  61. package/dist/integrations/pi/components/upstream/components/session-footer.js +12 -7
  62. package/dist/integrations/pi/components/upstream/history/editor-core.js +1 -1
  63. package/dist/integrations/pi/engine/adapter.d.ts +2 -1
  64. package/dist/integrations/pi/engine/adapter.js +181 -84
  65. package/dist/integrations/pi/engine/tool-rendering.d.ts +13 -0
  66. package/dist/integrations/pi/engine/tool-rendering.js +122 -0
  67. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +30 -0
  68. package/dist/integrations/pi/session-ui/prompt-chips.js +128 -1
  69. package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +8 -0
  70. package/dist/integrations/pi/session-ui/prompt-history-controller.js +30 -2
  71. package/dist/integrations/pi/session-ui/prompt-suggestion-controller.d.ts +11 -3
  72. package/dist/integrations/pi/session-ui/prompt-suggestion-controller.js +126 -51
  73. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +24 -2
  74. package/dist/integrations/pi/session-ui/session-shell-root.js +108 -38
  75. package/dist/integrations/pi/session-ui/session-shell.js +51 -31
  76. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +7 -1
  77. package/dist/integrations/pi/session-ui/session-viewport-controller.js +106 -16
  78. package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -0
  79. package/dist/integrations/pi/tui-runtime/adapter.js +35 -11
  80. package/dist/integrations/pi/tui-runtime/contracts.d.ts +4 -1
  81. package/dist/integrations/pi/tui-runtime/index.d.ts +1 -0
  82. package/dist/integrations/pi/tui-runtime/index.js +1 -0
  83. package/dist/integrations/pi/tui-runtime/mouse-report-input.d.ts +13 -0
  84. package/dist/integrations/pi/tui-runtime/mouse-report-input.js +59 -0
  85. package/dist/integrations/pi/tui-runtime/overlay-geometry.d.ts +32 -0
  86. package/dist/integrations/pi/tui-runtime/overlay-geometry.js +159 -0
  87. package/dist/native/darwin-arm64/manifest.json +1 -1
  88. package/dist/native/linux-x64/manifest.json +1 -1
  89. package/dist/native/win32-x64/manifest.json +2 -2
  90. package/dist/native/win32-x64/process-guardian.exe +0 -0
  91. package/dist/product-identity.d.ts +1 -1
  92. package/dist/product-identity.js +1 -1
  93. package/dist/product-identity.json +1 -0
  94. package/dist/ui/components/index.d.ts +1 -0
  95. package/dist/ui/components/index.js +1 -0
  96. package/dist/ui/components/line-input.d.ts +2 -3
  97. package/dist/ui/components/line-input.js +4 -20
  98. package/dist/ui/components/mouse.d.ts +1 -1
  99. package/dist/ui/components/mouse.js +4 -3
  100. package/dist/ui/components/prompt-input.d.ts +32 -0
  101. package/dist/ui/components/prompt-input.js +54 -0
  102. package/docs/architecture/boundaries.md +1 -0
  103. package/docs/architecture/history-editor-provenance.md +5 -1
  104. package/docs/architecture/project-structure.md +1 -0
  105. package/docs/architecture/prompt-suggestions.md +68 -0
  106. package/docs/architecture/toolchain.md +1 -1
  107. package/docs/architecture/ui-reference-provenance.md +23 -0
  108. package/docs/ci-release-runbook.md +45 -7
  109. package/docs/features/modal-content-interaction.md +37 -0
  110. package/package.json +1 -1
@@ -30,6 +30,36 @@ export declare class PromptChipStore {
30
30
  target: string;
31
31
  }[];
32
32
  expandCopiedText(text: string): string;
33
+ /**
34
+ * Recall-safe history text. Text / URL / file / folder chips expand to their resolved value
35
+ * so a restart can reuse them without the originating chip cache. Image chip tags are
36
+ * preserved verbatim so the sidecar-backed rehydration path can re-render them into live
37
+ * chips carrying the original attachment bytes.
38
+ */
33
39
  prepareHistoryText(text: string): string;
40
+ /**
41
+ * Extract the image chip attachments referenced by an editor draft in occurrence order,
42
+ * paired with their sidecar identifier. Callers use this to persist image sidecars before a
43
+ * history row commits, so a fresh process can rehydrate the same chip layout. Unresolved
44
+ * pending pastes and non-image chips are skipped; caller is responsible for waiting on
45
+ * pending readiness before this point.
46
+ */
47
+ imageChipAttachments(text: string): {
48
+ readonly id: string;
49
+ readonly tag: string;
50
+ readonly image: PromptImageAttachment;
51
+ }[];
52
+ /**
53
+ * Rehydrate a durable recall value into an editor-ready draft: image chip tags are looked up
54
+ * against a caller-supplied sidecar resolver, and detectable URL / file / folder / large text
55
+ * substrings are re-registered as atomic chips through the same identity rules the paste path
56
+ * uses. Image chips whose sidecar returns null are silently stripped without a placeholder.
57
+ *
58
+ * Every chip returned is registered in the session `#chips` map so atomic-range,
59
+ * hyperlink-range, and submission-expansion paths behave identically to a freshly typed draft.
60
+ * Callers should treat the returned string as programmatic editor text, not another draft to
61
+ * scan.
62
+ */
63
+ rehydrateHistoryText(text: string, resolveImage: (id: string) => PromptImageAttachment | null): string;
34
64
  prepareSubmission(text: string): PreparedPrompt;
35
65
  }
@@ -9,7 +9,13 @@ import { prepareTextPaste } from "./text-paste.js";
9
9
  const CHIP_PATTERN = /\[(?:paste #\d+ (?:\+\d+ lines|\d+ chars)|📷 [^\]]+|📁 [^\]]+|📄 [^\]]+|🖼 {1,2}[^\]]+|🔗 [^\]]+)\]/gu;
10
10
  const IMAGE_EXTENSION = /\.(?:jpe?g|png|webp|gif|bmp|tiff?)$/iu;
11
11
  const URL_PATTERN = /^https?:\/\/[^\s\u0000-\u001f\u007f]+$/iu;
12
+ const URL_SUBSTRING_PATTERN = /https?:\/\/[^\s\u0000-\u001f\u007f\]]+/giu;
13
+ const IMAGE_CHIP_IDENTIFIER_PATTERN = /^\[📷 screenshot-([a-f0-9]+)(?:-resized)?\]$/u;
12
14
  const URL_DISPLAY_LENGTH = 40;
15
+ function imageChipIdentifier(tag) {
16
+ const match = IMAGE_CHIP_IDENTIFIER_PATTERN.exec(tag);
17
+ return match === null ? null : match[1] ?? null;
18
+ }
13
19
  /** Owns semantic chips and bounded pending paste references; cancels background work on reset or disposal. */
14
20
  export class PromptChipStore {
15
21
  #chips = new Map();
@@ -208,8 +214,129 @@ export class PromptChipStore {
208
214
  expandCopiedText(text) {
209
215
  return this.#replaceResolvable(text, false).text;
210
216
  }
217
+ /**
218
+ * Recall-safe history text. Text / URL / file / folder chips expand to their resolved value
219
+ * so a restart can reuse them without the originating chip cache. Image chip tags are
220
+ * preserved verbatim so the sidecar-backed rehydration path can re-render them into live
221
+ * chips carrying the original attachment bytes.
222
+ */
211
223
  prepareHistoryText(text) {
212
- return this.#replaceResolvable(text, false, true).text.trim();
224
+ return this.#replaceResolvable(text, false, false).text.trim();
225
+ }
226
+ /**
227
+ * Extract the image chip attachments referenced by an editor draft in occurrence order,
228
+ * paired with their sidecar identifier. Callers use this to persist image sidecars before a
229
+ * history row commits, so a fresh process can rehydrate the same chip layout. Unresolved
230
+ * pending pastes and non-image chips are skipped; caller is responsible for waiting on
231
+ * pending readiness before this point.
232
+ */
233
+ imageChipAttachments(text) {
234
+ const results = [];
235
+ const seen = new Set();
236
+ for (const match of text.matchAll(CHIP_PATTERN)) {
237
+ const tag = match[0];
238
+ if (seen.has(tag))
239
+ continue;
240
+ const chip = this.#chips.get(tag);
241
+ if (chip === undefined || chip.kind !== "image")
242
+ continue;
243
+ const identifier = imageChipIdentifier(tag);
244
+ if (identifier === null)
245
+ continue;
246
+ seen.add(tag);
247
+ results.push({ id: identifier, tag, image: chip.image });
248
+ }
249
+ return results;
250
+ }
251
+ /**
252
+ * Rehydrate a durable recall value into an editor-ready draft: image chip tags are looked up
253
+ * against a caller-supplied sidecar resolver, and detectable URL / file / folder / large text
254
+ * substrings are re-registered as atomic chips through the same identity rules the paste path
255
+ * uses. Image chips whose sidecar returns null are silently stripped without a placeholder.
256
+ *
257
+ * Every chip returned is registered in the session `#chips` map so atomic-range,
258
+ * hyperlink-range, and submission-expansion paths behave identically to a freshly typed draft.
259
+ * Callers should treat the returned string as programmatic editor text, not another draft to
260
+ * scan.
261
+ */
262
+ rehydrateHistoryText(text, resolveImage) {
263
+ if (!text)
264
+ return text;
265
+ // Rationale: resolve image chip tags in-place before further classification. Sidecar hits
266
+ // register a live image chip; misses silently strip the tag (no placeholder, no notice) per
267
+ // the durability contract.
268
+ CHIP_PATTERN.lastIndex = 0;
269
+ const resolvedImages = text.replace(CHIP_PATTERN, tag => {
270
+ const identifier = imageChipIdentifier(tag);
271
+ if (identifier === null)
272
+ return tag;
273
+ const attachment = resolveImage(identifier);
274
+ if (attachment === null)
275
+ return "";
276
+ this.#chips.set(tag, { kind: "image", tag, image: attachment });
277
+ return tag;
278
+ });
279
+ // Rationale: split around any surviving image tags so paste-time classification runs on the
280
+ // intervening prose exactly as it would on a fresh clipboard payload, plus a substring URL
281
+ // scan so embedded links become atomic chips even in the middle of surrounding text.
282
+ CHIP_PATTERN.lastIndex = 0;
283
+ const segments = [];
284
+ let cursor = 0;
285
+ for (const match of resolvedImages.matchAll(CHIP_PATTERN)) {
286
+ const index = match.index ?? cursor;
287
+ if (index > cursor)
288
+ segments.push(resolvedImages.slice(cursor, index));
289
+ segments.push(match[0]);
290
+ cursor = index + match[0].length;
291
+ }
292
+ if (cursor < resolvedImages.length)
293
+ segments.push(resolvedImages.slice(cursor));
294
+ CHIP_PATTERN.lastIndex = 0;
295
+ return segments.map(segment => {
296
+ if (segment.length === 0 || CHIP_PATTERN.test(segment)) {
297
+ CHIP_PATTERN.lastIndex = 0;
298
+ return segment;
299
+ }
300
+ CHIP_PATTERN.lastIndex = 0;
301
+ // Invariant: preserve original whitespace framing so the concatenated recall value
302
+ // remains stable across rehydration.
303
+ const leading = segment.match(/^\s+/)?.[0] ?? "";
304
+ const trailing = segment.match(/\s+$/)?.[0] ?? "";
305
+ const body = segment.slice(leading.length, segment.length - trailing.length);
306
+ if (body.length === 0)
307
+ return segment;
308
+ // Rationale: substring URL rehydration reuses paste-time URL classification and identity
309
+ // rules, producing a URL chip whenever a bare http(s) URL appears in the prose. Whole-text
310
+ // classification runs afterwards to catch path / large-paste candidates that consume the
311
+ // entire remaining body.
312
+ const withUrlChips = this.#rehydrateUrlSubstrings(body);
313
+ if (withUrlChips !== body)
314
+ return `${leading}${withUrlChips}${trailing}`;
315
+ const classified = this.transformPastedContent({ kind: "text", text: body });
316
+ return `${leading}${classified}${trailing}`;
317
+ }).join("");
318
+ }
319
+ #rehydrateUrlSubstrings(body) {
320
+ let output = "";
321
+ let cursor = 0;
322
+ URL_SUBSTRING_PATTERN.lastIndex = 0;
323
+ for (;;) {
324
+ const match = URL_SUBSTRING_PATTERN.exec(body);
325
+ if (match === null)
326
+ break;
327
+ const url = match[0];
328
+ if (match.index > cursor)
329
+ output += body.slice(cursor, match.index);
330
+ const label = url.length <= URL_DISPLAY_LENGTH ? url : `${url.slice(0, URL_DISPLAY_LENGTH)}…`;
331
+ output += this.#recordUnique({ kind: "url", tag: `[🔗 ${label}]`, label, url });
332
+ cursor = match.index + url.length;
333
+ }
334
+ URL_SUBSTRING_PATTERN.lastIndex = 0;
335
+ if (cursor === 0)
336
+ return body;
337
+ if (cursor < body.length)
338
+ output += body.slice(cursor);
339
+ return output;
213
340
  }
214
341
  prepareSubmission(text) {
215
342
  const expanded = this.#replaceResolvable(text, true);
@@ -10,6 +10,10 @@ export declare class PromptHistoryController {
10
10
  fallback: readonly string[];
11
11
  active(): boolean;
12
12
  render(): void;
13
+ /** Applied to every recall text before it enters the editor recall list. Callers use this
14
+ * to re-classify durable chip content back into atomic chips; the identity map returned by
15
+ * default keeps historical behavior for callers that opt out. */
16
+ rehydrate?: (text: string) => string;
13
17
  };
14
18
  constructor(options: {
15
19
  editor: PiShellEditorPort;
@@ -18,6 +22,10 @@ export declare class PromptHistoryController {
18
22
  fallback: readonly string[];
19
23
  active(): boolean;
20
24
  render(): void;
25
+ /** Applied to every recall text before it enters the editor recall list. Callers use this
26
+ * to re-classify durable chip content back into atomic chips; the identity map returned by
27
+ * default keeps historical behavior for callers that opt out. */
28
+ rehydrate?: (text: string) => string;
21
29
  });
22
30
  start(): void;
23
31
  capture(text: string, kind: PromptHistoryKind, cwd: string, sessionId: string): void;
@@ -7,6 +7,7 @@ export class PromptHistoryController {
7
7
  #unsubscribe;
8
8
  #snapshot;
9
9
  #fallback;
10
+ #rehydrateCache;
10
11
  #disposed = false;
11
12
  #generation = 0;
12
13
  #unsubscribeSnapshot;
@@ -18,6 +19,7 @@ export class PromptHistoryController {
18
19
  if (options.editor.recall === undefined)
19
20
  throw new Error("The selected editor does not expose typed history");
20
21
  this.#snapshot = { revision: 0, limit: options.limit, entries: [] };
22
+ this.#rehydrateCache = new Map();
21
23
  this.#fallback = boundedTexts([...options.fallback].reverse(), options.limit);
22
24
  this.#unsubscribeSnapshot = this.#subscribeSnapshot();
23
25
  this.#unsubscribeFailure = options.store.onFailure(() => {
@@ -71,17 +73,43 @@ export class PromptHistoryController {
71
73
  synchronize() {
72
74
  if (this.#disposed || !this.options.active())
73
75
  return;
74
- const values = [...this.#local.values()].reverse().map(item => item.text)
75
- .concat(this.#snapshot.entries.map(item => item.text), this.#fallback);
76
+ const local = [...this.#local.values()].reverse().map(item => item.text);
77
+ const saved = this.#snapshot.entries.map(item => item.text);
78
+ const values = this.#rehydrateAll(local.concat(saved, this.#fallback));
76
79
  this.options.editor.recall.replace(boundedTexts(values, this.#snapshot.limit));
77
80
  this.options.render();
78
81
  }
82
+ #rehydrateAll(values) {
83
+ const rehydrate = this.options.rehydrate;
84
+ if (rehydrate === undefined)
85
+ return [...values];
86
+ const cache = this.#rehydrateCache;
87
+ const results = [];
88
+ for (const value of values) {
89
+ const cached = cache.get(value);
90
+ if (cached !== undefined) {
91
+ results.push(cached);
92
+ continue;
93
+ }
94
+ const rendered = rehydrate(value);
95
+ cache.set(value, rendered);
96
+ results.push(rendered);
97
+ }
98
+ if (cache.size > 512) {
99
+ // Performance: keep the cache bounded so long sessions cannot grow it without limit.
100
+ const keys = [...cache.keys()].slice(0, cache.size - 256);
101
+ for (const key of keys)
102
+ cache.delete(key);
103
+ }
104
+ return results;
105
+ }
79
106
  reset(fallback) {
80
107
  this.#generation++;
81
108
  this.#unsubscribeSnapshot();
82
109
  this.#unsubscribeSnapshot = this.#subscribeSnapshot();
83
110
  this.#fallback = boundedTexts([...fallback].reverse(), this.options.limit);
84
111
  this.#local.clear();
112
+ this.#rehydrateCache.clear();
85
113
  this.options.editor.recall.reset();
86
114
  this.synchronize();
87
115
  this.options.store.refresh();
@@ -1,6 +1,7 @@
1
- import { type OwnedUiPromptSuggestionGeneratorPort, type OwnedUiPromptSuggestionIdentity, type OwnedUiPromptSuggestionState } from "../../../contracts/owned-ui/index.js";
1
+ import { type OwnedUiPromptSuggestionGeneratorPort, type OwnedUiPromptSuggestionIdentity, type OwnedUiPromptSuggestionState, type SuggestionDecision, type SuggestionDecisionReason, type SuggestionDiagnosticObserver } from "../../../contracts/owned-ui/index.js";
2
2
  export interface ContextualPromptSuggestionSurface {
3
3
  canPresent(identity: OwnedUiPromptSuggestionIdentity): boolean;
4
+ presentationBlockReason?(identity: OwnedUiPromptSuggestionIdentity): SuggestionDecision;
4
5
  present(text: string): boolean;
5
6
  clear(): void;
6
7
  requestRender(): void;
@@ -10,18 +11,25 @@ export interface ContextualPromptSuggestionControllerOptions {
10
11
  readonly surface: ContextualPromptSuggestionSurface;
11
12
  readonly enabled: boolean;
12
13
  readonly timeoutMs?: number;
14
+ readonly diagnostics?: SuggestionDiagnosticObserver;
15
+ readonly now?: () => number;
13
16
  }
14
- /** Prefetches one candidate, holds it invisibly, and publishes only after matching settlement. */
17
+ type DecisionIdentity = Omit<OwnedUiPromptSuggestionIdentity, "model"> & {
18
+ readonly model: OwnedUiPromptSuggestionIdentity["model"] | null;
19
+ };
20
+ /** Prefetches one inert candidate; owns retirement even when a provider ignores abort. */
15
21
  export declare class ContextualPromptSuggestionController {
16
22
  #private;
17
23
  readonly options: ContextualPromptSuggestionControllerOptions;
18
24
  constructor(options: ContextualPromptSuggestionControllerOptions);
19
25
  get state(): OwnedUiPromptSuggestionState;
20
26
  setEnabled(enabled: boolean): void;
21
- consider(identity: OwnedUiPromptSuggestionIdentity, eligible: boolean): void;
27
+ skip(identity: DecisionIdentity, reason: SuggestionDecisionReason): void;
28
+ consider(identity: OwnedUiPromptSuggestionIdentity, decision: boolean | SuggestionDecision): void;
22
29
  settle(identity: OwnedUiPromptSuggestionIdentity): void;
23
30
  accept(): void;
24
31
  invalidate(): void;
25
32
  dispose(): void;
26
33
  }
27
34
  export declare function samePromptSuggestionIdentity(left: OwnedUiPromptSuggestionIdentity, right: OwnedUiPromptSuggestionIdentity): boolean;
35
+ export {};
@@ -1,18 +1,22 @@
1
1
  import { assertOwnedUiPromptSuggestionResult, normalizePromptSuggestionCandidate, } from "../../../contracts/owned-ui/index.js";
2
- /** Prefetches one candidate, holds it invisibly, and publishes only after matching settlement. */
2
+ /** Prefetches one inert candidate; owns retirement even when a provider ignores abort. */
3
3
  export class ContextualPromptSuggestionController {
4
4
  options;
5
5
  #state = { status: "idle" };
6
- #epoch = 0;
7
- #abort = null;
6
+ #request = null;
8
7
  #lastConsidered = "";
9
8
  #enabled;
10
9
  #disposed = false;
10
+ #sessionKey = "";
11
+ #sessionSequence = 0;
12
+ #requestSequence = 0;
11
13
  #timeoutMs;
14
+ #now;
12
15
  constructor(options) {
13
16
  this.options = options;
14
17
  this.#enabled = options.enabled;
15
18
  this.#timeoutMs = options.timeoutMs ?? 15_000;
19
+ this.#now = options.now ?? Date.now;
16
20
  }
17
21
  get state() { return this.#state; }
18
22
  setEnabled(enabled) {
@@ -21,103 +25,174 @@ export class ContextualPromptSuggestionController {
21
25
  this.#enabled = enabled;
22
26
  this.invalidate();
23
27
  }
24
- consider(identity, eligible) {
28
+ skip(identity, reason) {
29
+ this.invalidate();
30
+ this.#emit(this.#metadata(identity), "skipped", reason);
31
+ }
32
+ consider(identity, decision) {
25
33
  const key = identityKey(identity);
26
34
  if (key === this.#lastConsidered)
27
35
  return;
28
36
  this.#lastConsidered = key;
29
37
  this.invalidate();
30
- if (!this.#enabled || this.#disposed || !eligible)
38
+ const reason = this.#disposed ? "disposed" : !this.#enabled ? "disabled"
39
+ : decision === false ? "ineligible" : decision === true ? null : decision;
40
+ if (reason !== null) {
41
+ this.#emit(this.#metadata(identity), "skipped", reason);
31
42
  return;
32
- const epoch = this.#epoch;
33
- const abort = new AbortController();
34
- this.#abort = abort;
43
+ }
44
+ let reasoning = "unavailable";
45
+ try {
46
+ reasoning = this.options.generator.suggestionReasoningPolicy?.() ?? "unavailable";
47
+ }
48
+ catch { /* Security: isolate diagnostic metadata failure. */ }
49
+ const request = {
50
+ identity,
51
+ metadata: { ...this.#metadata(identity), request: ++this.#requestSequence, reasoning },
52
+ abort: new AbortController(), startedAt: this.#now(), terminal: false,
53
+ };
54
+ this.#request = request;
35
55
  this.#state = { status: "generating", identity, settled: false };
36
- const timeout = setTimeout(() => {
37
- abort.abort();
38
- if (this.#epoch === epoch && this.#state.status === "generating") {
39
- this.#abort = null;
40
- this.#state = { status: "idle" };
41
- }
56
+ this.#emit(request.metadata, "started");
57
+ request.timer = setTimeout(() => {
58
+ if (this.#request !== request || this.#state.status !== "generating")
59
+ return;
60
+ this.#finish(request, "timeout");
61
+ this.#state = { status: "idle" };
62
+ this.#request = null;
63
+ request.abort.abort();
42
64
  }, this.#timeoutMs);
43
- timeout.unref?.();
44
- void this.options.generator.generate({ identity, signal: abort.signal })
45
- .then(result => this.#receive(epoch, identity, result))
46
- .catch(() => this.#fail(epoch))
47
- .finally(() => {
48
- clearTimeout(timeout);
49
- if (this.#epoch === epoch)
50
- this.#abort = null;
51
- });
65
+ request.timer.unref?.();
66
+ try {
67
+ void this.options.generator.generate({ identity, signal: request.abort.signal })
68
+ .then(result => this.#receive(request, result), () => this.#failed(request))
69
+ .finally(() => clearTimeout(request.timer));
70
+ }
71
+ catch {
72
+ this.#failed(request);
73
+ }
52
74
  }
53
75
  settle(identity) {
54
76
  if (this.#disposed || !this.#enabled || this.#state.status === "idle")
55
77
  return;
56
78
  if (!samePromptSuggestionIdentity(this.#state.identity, identity)) {
79
+ if (this.#request)
80
+ this.#finish(this.#request, "stale-result", "stale-identity");
57
81
  this.invalidate();
58
82
  return;
59
83
  }
60
- if (this.#state.status === "generating") {
84
+ if (this.#state.status === "generating")
61
85
  this.#state = { ...this.#state, settled: true };
62
- return;
63
- }
64
- if (this.#state.status === "prepared")
65
- this.#show(identity, this.#state.text);
66
- }
67
- accept() {
68
- this.#invalidate(false);
69
- }
70
- invalidate() {
71
- this.#invalidate(true);
86
+ else if (this.#state.status === "prepared" && this.#request)
87
+ this.#show(this.#request, this.#state.text);
72
88
  }
89
+ accept() { this.#invalidate(false); }
90
+ invalidate() { this.#invalidate(true); }
73
91
  dispose() {
74
92
  this.#disposed = true;
75
93
  this.invalidate();
94
+ this.#lastConsidered = "";
95
+ this.#sessionKey = "";
76
96
  }
77
97
  #invalidate(clearSurface) {
78
98
  const hadVisibleSuggestion = this.#state.status === "available";
79
- this.#epoch += 1;
80
- this.#abort?.abort();
81
- this.#abort = null;
99
+ const request = this.#request;
100
+ if (request)
101
+ this.#finish(request, "cancelled");
102
+ this.#request = null;
82
103
  this.#state = { status: "idle" };
104
+ request?.abort.abort();
83
105
  if (clearSurface && hadVisibleSuggestion) {
84
106
  this.options.surface.clear();
85
107
  this.options.surface.requestRender();
86
108
  }
87
109
  }
88
- #fail(epoch) {
89
- if (this.#epoch !== epoch)
110
+ #failed(request) {
111
+ if (this.#request !== request || request.terminal) {
112
+ this.#late(request);
90
113
  return;
91
- this.#abort = null;
114
+ }
115
+ this.#finish(request, "provider-failure");
116
+ this.#request = null;
92
117
  this.#state = { status: "idle" };
93
118
  }
94
- #receive(epoch, identity, result) {
95
- assertOwnedUiPromptSuggestionResult(result);
96
- if (this.#disposed || !this.#enabled || this.#epoch !== epoch || this.#state.status !== "generating")
119
+ #receive(request, result) {
120
+ if (this.#disposed || this.#request !== request || request.terminal || this.#state.status !== "generating") {
121
+ this.#late(request);
97
122
  return;
98
- if (!samePromptSuggestionIdentity(identity, result.identity)) {
123
+ }
124
+ try {
125
+ assertOwnedUiPromptSuggestionResult(result);
126
+ }
127
+ catch {
128
+ this.#finish(request, "rejected");
129
+ this.invalidate();
130
+ return;
131
+ }
132
+ if (!samePromptSuggestionIdentity(request.identity, result.identity)) {
133
+ this.#finish(request, "stale-result", "stale-identity");
134
+ this.invalidate();
135
+ return;
136
+ }
137
+ if (result.outcome !== "candidate") {
138
+ this.#finish(request, result.outcome);
99
139
  this.invalidate();
100
140
  return;
101
141
  }
102
142
  const text = normalizePromptSuggestionCandidate(result.text);
103
143
  if (text === null) {
104
- this.#fail(epoch);
144
+ this.#finish(request, "rejected");
145
+ this.invalidate();
105
146
  return;
106
147
  }
107
- this.#abort = null;
148
+ clearTimeout(request.timer);
108
149
  if (this.#state.settled)
109
- this.#show(identity, text);
150
+ this.#show(request, text);
110
151
  else
111
- this.#state = { status: "prepared", identity, text };
152
+ this.#state = { status: "prepared", identity: request.identity, text };
112
153
  }
113
- #show(identity, text) {
114
- if (!this.options.surface.canPresent(identity) || !this.options.surface.present(text)) {
154
+ #show(request, text) {
155
+ const reason = this.options.surface.presentationBlockReason?.(request.identity)
156
+ ?? (this.options.surface.canPresent(request.identity) ? null : "presentation-unavailable");
157
+ if (reason !== null || !this.options.surface.present(text)) {
158
+ this.#finish(request, "presentation-blocked", reason ?? "presentation-unavailable");
115
159
  this.invalidate();
116
160
  return;
117
161
  }
118
- this.#state = { status: "available", identity, text };
162
+ this.#state = { status: "available", identity: request.identity, text };
163
+ this.#finish(request, "displayed");
119
164
  this.options.surface.requestRender();
120
165
  }
166
+ #finish(request, event, reason) {
167
+ if (request.terminal)
168
+ return;
169
+ // Concurrency: retire before notifying observers or aborting; neither may rewrite the winner.
170
+ request.terminal = true;
171
+ clearTimeout(request.timer);
172
+ this.#emit({ ...request.metadata, elapsedMs: Math.max(0, this.#now() - request.startedAt) }, event, reason);
173
+ }
174
+ #late(request) {
175
+ if (!this.#disposed)
176
+ this.#emit({ ...request.metadata, elapsedMs: Math.max(0, this.#now() - request.startedAt) }, "late-result-discarded");
177
+ }
178
+ #metadata(identity) {
179
+ const key = `${identity.sessionId}\0${identity.sessionGeneration}`;
180
+ if (this.#sessionKey !== key) {
181
+ this.#sessionKey = key;
182
+ this.#sessionSequence++;
183
+ }
184
+ return {
185
+ event: "skipped", session: this.#sessionSequence, run: identity.runSequence, response: identity.responseSequence,
186
+ request: 0, provider: identity.model?.providerId ?? "", model: identity.model?.modelId ?? "",
187
+ reasoning: "unavailable", elapsedMs: 0,
188
+ };
189
+ }
190
+ #emit(metadata, event, reason) {
191
+ try {
192
+ this.options.diagnostics?.record({ ...metadata, event, ...(reason === undefined ? {} : { reason }) });
193
+ }
194
+ catch { /* Security: diagnostics never own input or lifecycle. */ }
195
+ }
121
196
  }
122
197
  export function samePromptSuggestionIdentity(left, right) {
123
198
  return left.sessionId === right.sessionId
@@ -1,4 +1,5 @@
1
- import type { OwnedUiPromptSuggestionGeneratorPort, OwnedUiSessionViewModel, OwnedUiViewportSettings, OwnedUiViewportSettingsPort } from "../../../contracts/owned-ui/index.js";
1
+ import type { OwnedUiPromptSuggestionGeneratorPort, OwnedUiSessionViewModel, OwnedUiViewportSettings, OwnedUiViewportSettingsPort, SuggestionDecision, SuggestionDiagnosticObserver } from "../../../contracts/owned-ui/index.js";
2
+ import type { PiTuiPointerSurface } from "../tui-runtime/index.js";
2
3
  import type { UiRouteHost } from "../../../ui/apps/index.js";
3
4
  import { type TranscriptViewportFrame, type TranscriptViewportFrameDescriptor } from "../../../ui/components/index.js";
4
5
  import { type PiEngineAdapter, type PiWorkflowMessage, type PiWorkflowResult } from "../engine/index.js";
@@ -22,6 +23,10 @@ export interface OwnedUiSessionShellOptions {
22
23
  readonly store: import("../../../contracts/owned-ui/index.js").PromptHistoryPort;
23
24
  readonly limit: number;
24
25
  readonly editor: import("../components/index.js").HistoryEditorConstructor;
26
+ /** Optional per-profile filesystem sidecar for image chip payloads. When present, submit
27
+ * writes each referenced attachment and recall reads them back to rehydrate live chips.
28
+ * When absent, image chips silently strip on recall, matching the missing-sidecar path. */
29
+ readonly imageSidecar?: import("../../../contracts/owned-ui/index.js").PromptHistoryImageSidecarPort;
25
30
  };
26
31
  readonly backend: OwnedUiBackendPort;
27
32
  readonly cwd: string;
@@ -45,6 +50,7 @@ export interface OwnedUiSessionShellOptions {
45
50
  };
46
51
  /** Optional deterministic seam for keyboard scheduling and phase evidence. */
47
52
  readonly promptSuggestions?: {
53
+ readonly diagnostics?: SuggestionDiagnosticObserver;
48
54
  readonly generator: OwnedUiPromptSuggestionGeneratorPort;
49
55
  readonly enabled: () => boolean;
50
56
  readonly onChange: (listener: (enabled: boolean) => void) => () => void;
@@ -99,12 +105,24 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
99
105
  setMermaidRenderingMode(mode: "off" | "final" | "streaming"): void;
100
106
  setImagePresentation(showImages: boolean, imageWidthCells: number): void;
101
107
  prepareHistoryText(text: string): string;
108
+ rehydrateHistoryText(text: string, resolveImage: (id: string) => import("../../../contracts/owned-ui/index.js").PromptHistoryImageSidecarAttachment | null): string;
109
+ imageChipAttachmentsFromEditor(text: string): {
110
+ readonly id: string;
111
+ readonly tag: string;
112
+ readonly image: {
113
+ readonly type: "image";
114
+ readonly data: string;
115
+ readonly mimeType: string;
116
+ };
117
+ }[];
102
118
  preparePromptSubmission(text: string): PreparedPrompt;
103
119
  hasPendingPastes(text: string): boolean;
104
120
  waitForPromptPastes(text: string, signal: AbortSignal): Promise<PreparedPrompt>;
105
121
  resetPendingPastes(): void;
106
122
  disposePendingPastes(): Promise<void>;
123
+ promptSuggestionPrepareBlockReason(): SuggestionDecision;
107
124
  canPreparePromptSuggestion(): boolean;
125
+ promptSuggestionPresentationBlockReason(): SuggestionDecision;
108
126
  canPresentPromptSuggestion(): boolean;
109
127
  setPromptSuggestion(text: string | null): void;
110
128
  update(view: OwnedUiSessionViewModel): void;
@@ -147,9 +165,13 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
147
165
  setViewportConfig(config: OwnedUiViewportSettings): void;
148
166
  resumeViewportFollowing(): void;
149
167
  noteCompletedAssistantMessage(): void;
168
+ /** A new session binding owns new mounts even when it reuses semantic invocation ids. */
169
+ resetTranscript(): void;
150
170
  resetViewport(): void;
151
171
  clearViewportPointerState(): void;
152
- handleViewportPreInput(data: string, allowWheel?: boolean, now?: number): {
172
+ setViewportOverlaySurfaces(surfaces: readonly PiTuiPointerSurface[] | null): void;
173
+ viewportInputGeometryReady(width: number, height: number): boolean;
174
+ handleViewportPreInput(data: string, allowWheel?: boolean, now?: number, editorActive?: boolean): {
153
175
  readonly data: string;
154
176
  readonly consumed: boolean;
155
177
  readonly copyText?: string;