@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.
- package/README.md +40 -11
- package/dist/composition/owned-ui.d.ts +2 -0
- package/dist/composition/owned-ui.js +20 -3
- package/dist/contracts/owned-ui/index.d.ts +2 -0
- package/dist/contracts/owned-ui/index.js +2 -0
- package/dist/contracts/owned-ui/model.d.ts +38 -3
- package/dist/contracts/owned-ui/prompt-history.d.ts +18 -0
- package/dist/contracts/owned-ui/prompt-suggestions.d.ts +1 -1
- package/dist/contracts/owned-ui/prompt-suggestions.js +4 -1
- package/dist/contracts/owned-ui/suggestion-diagnostics.d.ts +22 -0
- package/dist/contracts/owned-ui/suggestion-diagnostics.js +9 -0
- package/dist/contracts/owned-ui/transcript-lifecycle.d.ts +5 -0
- package/dist/contracts/owned-ui/transcript-lifecycle.js +33 -0
- package/dist/contracts/owned-ui/validation.js +45 -2
- package/dist/contracts/presentation/index.d.ts +8 -0
- package/dist/features/owned-ui/settings-app.js +1 -1
- package/dist/features/prompt-history/image-sidecar.d.ts +32 -0
- package/dist/features/prompt-history/image-sidecar.js +130 -0
- package/dist/features/prompt-history/index.d.ts +2 -0
- package/dist/features/prompt-history/index.js +1 -0
- package/dist/features/prompt-history/paths.d.ts +1 -0
- package/dist/features/prompt-history/paths.js +2 -1
- package/dist/features/prompt-history/store.d.ts +1 -1
- package/dist/features/prompt-history/store.js +46 -2
- package/dist/features/prompt-history/worker.js +1 -1
- package/dist/features/prompt-suggestions/diagnostics.d.ts +18 -0
- package/dist/features/prompt-suggestions/diagnostics.js +82 -0
- package/dist/features/prompt-suggestions/index.d.ts +1 -0
- package/dist/features/prompt-suggestions/index.js +1 -0
- package/dist/foundation/release/bootstrap.d.ts +2 -0
- package/dist/foundation/release/bootstrap.js +54 -18
- package/dist/foundation/release/index.d.ts +1 -0
- package/dist/foundation/release/index.js +1 -0
- package/dist/foundation/release/update-launch.d.ts +6 -0
- package/dist/foundation/release/update-launch.js +17 -0
- package/dist/foundation/release/update.d.ts +2 -0
- package/dist/foundation/release/update.js +20 -21
- package/dist/foundation/supervision/main.js +5 -2
- package/dist/foundation/supervision/server.js +40 -13
- package/dist/integrations/pi/components/owned-editor-ux.d.ts +9 -1
- package/dist/integrations/pi/components/owned-editor-ux.js +11 -8
- package/dist/integrations/pi/components/prompt-input-port.d.ts +18 -0
- package/dist/integrations/pi/components/prompt-input-port.js +1 -0
- package/dist/integrations/pi/components/shell-editor-autocomplete.js +66 -11
- package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
- package/dist/integrations/pi/components/shell-footer-status.js +9 -5
- package/dist/integrations/pi/components/shell-presenters-info.js +7 -2
- package/dist/integrations/pi/components/shell-presenters-transcript.d.ts +6 -2
- package/dist/integrations/pi/components/shell-presenters-transcript.js +81 -49
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +17 -4
- package/dist/integrations/pi/components/shell-shared-facade.js +2 -1
- package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +3 -1
- package/dist/integrations/pi/components/submitted-prompt-adapter.js +20 -4
- package/dist/integrations/pi/components/tool-result-adapter.d.ts +6 -0
- package/dist/integrations/pi/components/tool-result-adapter.js +27 -0
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -0
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +5 -0
- package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +7 -2
- package/dist/integrations/pi/components/upstream/components/owned-editor.js +41 -33
- package/dist/integrations/pi/components/upstream/components/session-footer.d.ts +2 -1
- package/dist/integrations/pi/components/upstream/components/session-footer.js +12 -7
- package/dist/integrations/pi/components/upstream/history/editor-core.js +1 -1
- package/dist/integrations/pi/engine/adapter.d.ts +2 -1
- package/dist/integrations/pi/engine/adapter.js +181 -84
- package/dist/integrations/pi/engine/tool-rendering.d.ts +13 -0
- package/dist/integrations/pi/engine/tool-rendering.js +122 -0
- package/dist/integrations/pi/session-ui/prompt-chips.d.ts +30 -0
- package/dist/integrations/pi/session-ui/prompt-chips.js +128 -1
- package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +8 -0
- package/dist/integrations/pi/session-ui/prompt-history-controller.js +30 -2
- package/dist/integrations/pi/session-ui/prompt-suggestion-controller.d.ts +11 -3
- package/dist/integrations/pi/session-ui/prompt-suggestion-controller.js +126 -51
- package/dist/integrations/pi/session-ui/session-shell-root.d.ts +24 -2
- package/dist/integrations/pi/session-ui/session-shell-root.js +108 -38
- package/dist/integrations/pi/session-ui/session-shell.js +51 -31
- package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +7 -1
- package/dist/integrations/pi/session-ui/session-viewport-controller.js +106 -16
- package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -0
- package/dist/integrations/pi/tui-runtime/adapter.js +35 -11
- package/dist/integrations/pi/tui-runtime/contracts.d.ts +4 -1
- package/dist/integrations/pi/tui-runtime/index.d.ts +1 -0
- package/dist/integrations/pi/tui-runtime/index.js +1 -0
- package/dist/integrations/pi/tui-runtime/mouse-report-input.d.ts +13 -0
- package/dist/integrations/pi/tui-runtime/mouse-report-input.js +59 -0
- package/dist/integrations/pi/tui-runtime/overlay-geometry.d.ts +32 -0
- package/dist/integrations/pi/tui-runtime/overlay-geometry.js +159 -0
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/product-identity.d.ts +1 -1
- package/dist/product-identity.js +1 -1
- package/dist/product-identity.json +1 -0
- package/dist/ui/components/index.d.ts +1 -0
- package/dist/ui/components/index.js +1 -0
- package/dist/ui/components/line-input.d.ts +2 -3
- package/dist/ui/components/line-input.js +4 -20
- package/dist/ui/components/mouse.d.ts +1 -1
- package/dist/ui/components/mouse.js +4 -3
- package/dist/ui/components/prompt-input.d.ts +32 -0
- package/dist/ui/components/prompt-input.js +54 -0
- package/docs/architecture/boundaries.md +1 -0
- package/docs/architecture/history-editor-provenance.md +5 -1
- package/docs/architecture/project-structure.md +1 -0
- package/docs/architecture/prompt-suggestions.md +68 -0
- package/docs/architecture/toolchain.md +1 -1
- package/docs/architecture/ui-reference-provenance.md +23 -0
- package/docs/ci-release-runbook.md +45 -7
- package/docs/features/modal-content-interaction.md +37 -0
- 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,
|
|
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
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
if (this.#
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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.#
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
#
|
|
89
|
-
if (this.#
|
|
110
|
+
#failed(request) {
|
|
111
|
+
if (this.#request !== request || request.terminal) {
|
|
112
|
+
this.#late(request);
|
|
90
113
|
return;
|
|
91
|
-
|
|
114
|
+
}
|
|
115
|
+
this.#finish(request, "provider-failure");
|
|
116
|
+
this.#request = null;
|
|
92
117
|
this.#state = { status: "idle" };
|
|
93
118
|
}
|
|
94
|
-
#receive(
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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.#
|
|
144
|
+
this.#finish(request, "rejected");
|
|
145
|
+
this.invalidate();
|
|
105
146
|
return;
|
|
106
147
|
}
|
|
107
|
-
|
|
148
|
+
clearTimeout(request.timer);
|
|
108
149
|
if (this.#state.settled)
|
|
109
|
-
this.#show(
|
|
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(
|
|
114
|
-
|
|
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
|
-
|
|
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;
|