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

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 (63) hide show
  1. package/README.md +9 -0
  2. package/dist/composition/owned-ui.js +12 -1
  3. package/dist/contracts/owned-ui/index.d.ts +1 -0
  4. package/dist/contracts/owned-ui/index.js +1 -0
  5. package/dist/contracts/owned-ui/prompt-history.d.ts +37 -0
  6. package/dist/contracts/owned-ui/prompt-history.js +23 -0
  7. package/dist/features/prompt-history/index.d.ts +2 -0
  8. package/dist/features/prompt-history/index.js +2 -0
  9. package/dist/features/prompt-history/paths.d.ts +4 -0
  10. package/dist/features/prompt-history/paths.js +16 -0
  11. package/dist/features/prompt-history/service.d.ts +24 -0
  12. package/dist/features/prompt-history/service.js +260 -0
  13. package/dist/features/prompt-history/store.d.ts +15 -0
  14. package/dist/features/prompt-history/store.js +166 -0
  15. package/dist/features/prompt-history/worker.d.ts +9 -0
  16. package/dist/features/prompt-history/worker.js +42 -0
  17. package/dist/integrations/pi/components/editor-interaction.d.ts +38 -0
  18. package/dist/integrations/pi/components/editor-interaction.js +1 -0
  19. package/dist/integrations/pi/components/history-editor-loader.d.ts +5 -0
  20. package/dist/integrations/pi/components/history-editor-loader.js +5 -0
  21. package/dist/integrations/pi/components/index.d.ts +1 -0
  22. package/dist/integrations/pi/components/index.js +1 -0
  23. package/dist/integrations/pi/components/owned-editor-ux.d.ts +1 -1
  24. package/dist/integrations/pi/components/owned-editor-ux.js +41 -19
  25. package/dist/integrations/pi/components/shell-editor-autocomplete.js +9 -1
  26. package/dist/integrations/pi/components/shell-shared-facade.d.ts +6 -0
  27. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +13 -9
  28. package/dist/integrations/pi/components/upstream/components/owned-editor.js +137 -133
  29. package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +280 -0
  30. package/dist/integrations/pi/components/upstream/history/editor-core.js +2062 -0
  31. package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +32 -0
  32. package/dist/integrations/pi/components/upstream/history/kill-ring.js +48 -0
  33. package/dist/integrations/pi/components/upstream/history/printable-key.d.ts +1 -0
  34. package/dist/integrations/pi/components/upstream/history/printable-key.js +40 -0
  35. package/dist/integrations/pi/components/upstream/history/text-helpers.d.ts +14 -0
  36. package/dist/integrations/pi/components/upstream/history/text-helpers.js +28 -0
  37. package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +22 -0
  38. package/dist/integrations/pi/components/upstream/history/undo-stack.js +36 -0
  39. package/dist/integrations/pi/components/upstream/history/word-navigation.d.ts +24 -0
  40. package/dist/integrations/pi/components/upstream/history/word-navigation.js +100 -0
  41. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +1 -0
  42. package/dist/integrations/pi/session-ui/prompt-chips.js +3 -0
  43. package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +30 -0
  44. package/dist/integrations/pi/session-ui/prompt-history-controller.js +147 -0
  45. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +8 -0
  46. package/dist/integrations/pi/session-ui/session-shell-root.js +3 -0
  47. package/dist/integrations/pi/session-ui/session-shell.js +40 -5
  48. package/dist/native/darwin-arm64/manifest.json +2 -2
  49. package/dist/native/darwin-arm64/process-guardian +0 -0
  50. package/dist/native/linux-x64/manifest.json +1 -1
  51. package/dist/native/win32-x64/manifest.json +2 -2
  52. package/dist/native/win32-x64/process-guardian.exe +0 -0
  53. package/dist/product-identity.d.ts +1 -1
  54. package/dist/product-identity.js +1 -1
  55. package/dist/product-identity.json +1 -0
  56. package/dist/ui/settings/declarations.js +18 -0
  57. package/dist/ui/settings/session.js +13 -4
  58. package/docs/architecture/boundaries.md +1 -0
  59. package/docs/architecture/history-editor-provenance.md +96 -0
  60. package/docs/architecture/project-structure.md +1 -0
  61. package/docs/architecture/resource-and-data-policy.md +22 -1
  62. package/docs/features/prompt-history.md +121 -0
  63. package/package.json +1 -1
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Adapted from @earendil-works/pi-tui 0.84.2, packages/tui/src/kill-ring.ts (MIT).
3
+ * Source commit: 914cf1472e715297caa30db4b9535d534a9eb718.
4
+ * Modifications: public imports and owned editor-local typed seams; see docs/architecture/history-editor-provenance.md.
5
+ */
6
+ /**
7
+ * Ring buffer for Emacs-style kill/yank operations.
8
+ *
9
+ * Tracks killed (deleted) text entries. Consecutive kills can accumulate
10
+ * into a single entry. Supports yank (paste most recent) and yank-pop
11
+ * (cycle through older entries).
12
+ */
13
+ export declare class KillRing {
14
+ private ring;
15
+ /**
16
+ * Add text to the kill ring.
17
+ *
18
+ * @param text - The killed text to add
19
+ * @param opts - Push options
20
+ * @param opts.prepend - If accumulating, prepend (backward deletion) or append (forward deletion)
21
+ * @param opts.accumulate - Merge with the most recent entry instead of creating a new one
22
+ */
23
+ push(text: string, opts: {
24
+ prepend: boolean;
25
+ accumulate?: boolean;
26
+ }): void;
27
+ /** Get most recent entry without modifying the ring. */
28
+ peek(): string | undefined;
29
+ /** Move last entry to front (for yank-pop cycling). */
30
+ rotate(): void;
31
+ get length(): number;
32
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Adapted from @earendil-works/pi-tui 0.84.2, packages/tui/src/kill-ring.ts (MIT).
3
+ * Source commit: 914cf1472e715297caa30db4b9535d534a9eb718.
4
+ * Modifications: public imports and owned editor-local typed seams; see docs/architecture/history-editor-provenance.md.
5
+ */
6
+ /**
7
+ * Ring buffer for Emacs-style kill/yank operations.
8
+ *
9
+ * Tracks killed (deleted) text entries. Consecutive kills can accumulate
10
+ * into a single entry. Supports yank (paste most recent) and yank-pop
11
+ * (cycle through older entries).
12
+ */
13
+ export class KillRing {
14
+ ring = [];
15
+ /**
16
+ * Add text to the kill ring.
17
+ *
18
+ * @param text - The killed text to add
19
+ * @param opts - Push options
20
+ * @param opts.prepend - If accumulating, prepend (backward deletion) or append (forward deletion)
21
+ * @param opts.accumulate - Merge with the most recent entry instead of creating a new one
22
+ */
23
+ push(text, opts) {
24
+ if (!text)
25
+ return;
26
+ if (opts.accumulate && this.ring.length > 0) {
27
+ const last = this.ring.pop();
28
+ this.ring.push(opts.prepend ? text + last : last + text);
29
+ }
30
+ else {
31
+ this.ring.push(text);
32
+ }
33
+ }
34
+ /** Get most recent entry without modifying the ring. */
35
+ peek() {
36
+ return this.ring.length > 0 ? this.ring[this.ring.length - 1] : undefined;
37
+ }
38
+ /** Move last entry to front (for yank-pop cycling). */
39
+ rotate() {
40
+ if (this.ring.length > 1) {
41
+ const last = this.ring.pop();
42
+ this.ring.unshift(last);
43
+ }
44
+ }
45
+ get length() {
46
+ return this.ring.length;
47
+ }
48
+ }
@@ -0,0 +1 @@
1
+ export declare function decodePrintableKey(data: string): string | undefined;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Adapted from @earendil-works/pi-tui 0.84.2, packages/tui/src/keys.ts (MIT).
3
+ * Source commit: 914cf1472e715297caa30db4b9535d534a9eb718.
4
+ * Modifications: public imports and owned editor-local typed seams; see docs/architecture/history-editor-provenance.md.
5
+ */
6
+ import { decodeKittyPrintable } from "#pi-tui";
7
+ const MODIFIERS = {
8
+ shift: 1,
9
+ alt: 2,
10
+ ctrl: 4,
11
+ super: 8,
12
+ };
13
+ const LOCK_MASK = 64 + 128;
14
+ function parseModifyOtherKeysSequence(data) {
15
+ const match = data.match(/^\x1b\[27;(\d+);(\d+)~$/);
16
+ if (!match)
17
+ return null;
18
+ const modValue = parseInt(match[1], 10);
19
+ const codepoint = parseInt(match[2], 10);
20
+ return { codepoint, modifier: modValue - 1 };
21
+ }
22
+ function decodeModifyOtherKeysPrintable(data) {
23
+ const parsed = parseModifyOtherKeysSequence(data);
24
+ if (!parsed)
25
+ return undefined;
26
+ const modifier = parsed.modifier & ~LOCK_MASK;
27
+ if ((modifier & ~MODIFIERS.shift) !== 0)
28
+ return undefined;
29
+ if (!Number.isFinite(parsed.codepoint) || parsed.codepoint < 32)
30
+ return undefined;
31
+ try {
32
+ return String.fromCodePoint(parsed.codepoint);
33
+ }
34
+ catch {
35
+ return undefined;
36
+ }
37
+ }
38
+ export function decodePrintableKey(data) {
39
+ return decodeKittyPrintable(data) ?? decodeModifyOtherKeysPrintable(data);
40
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Get the shared grapheme segmenter instance.
3
+ */
4
+ export declare function getGraphemeSegmenter(): Intl.Segmenter;
5
+ /**
6
+ * Get the shared word segmenter instance.
7
+ */
8
+ export declare function getWordSegmenter(): Intl.Segmenter;
9
+ export declare const cjkBreakRegex: RegExp;
10
+ export declare const PUNCTUATION_REGEX: RegExp;
11
+ /**
12
+ * Check if a character is whitespace.
13
+ */
14
+ export declare function isWhitespaceChar(char: string): boolean;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Adapted from @earendil-works/pi-tui 0.84.2, packages/tui/src/utils.ts (MIT).
3
+ * Source commit: 914cf1472e715297caa30db4b9535d534a9eb718.
4
+ * Modifications: public imports and owned editor-local typed seams; see docs/architecture/history-editor-provenance.md.
5
+ */
6
+ // segmenters (shared instance)
7
+ const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
8
+ const wordSegmenter = new Intl.Segmenter(undefined, { granularity: "word" });
9
+ /**
10
+ * Get the shared grapheme segmenter instance.
11
+ */
12
+ export function getGraphemeSegmenter() {
13
+ return graphemeSegmenter;
14
+ }
15
+ /**
16
+ * Get the shared word segmenter instance.
17
+ */
18
+ export function getWordSegmenter() {
19
+ return wordSegmenter;
20
+ }
21
+ export const cjkBreakRegex = /[\p{Script_Extensions=Han}\p{Script_Extensions=Hiragana}\p{Script_Extensions=Katakana}\p{Script_Extensions=Hangul}\p{Script_Extensions=Bopomofo}]/u;
22
+ export const PUNCTUATION_REGEX = /[(){}[\]<>.,;:'"!?+\-=*/\\|&%^$#@~`]/;
23
+ /**
24
+ * Check if a character is whitespace.
25
+ */
26
+ export function isWhitespaceChar(char) {
27
+ return /\s/.test(char);
28
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Adapted from @earendil-works/pi-tui 0.84.2, packages/tui/src/undo-stack.ts (MIT).
3
+ * Source commit: 914cf1472e715297caa30db4b9535d534a9eb718.
4
+ * Modifications: public imports and owned editor-local typed seams; see docs/architecture/history-editor-provenance.md.
5
+ */
6
+ /**
7
+ * Generic undo stack with clone-on-push semantics.
8
+ *
9
+ * Stores deep clones of state snapshots. Popped snapshots are returned
10
+ * directly (no re-cloning) since they are already detached.
11
+ */
12
+ export declare class UndoStack<S> {
13
+ private stack;
14
+ /** Push a deep clone of the given state onto the stack. */
15
+ push(state: S): void;
16
+ /** Pop and return the most recent snapshot, or undefined if empty. */
17
+ pop(): S | undefined;
18
+ /** Remove all snapshots. */
19
+ clear(): void;
20
+ updateSnapshots(update: (snapshot: S) => void): void;
21
+ get length(): number;
22
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Adapted from @earendil-works/pi-tui 0.84.2, packages/tui/src/undo-stack.ts (MIT).
3
+ * Source commit: 914cf1472e715297caa30db4b9535d534a9eb718.
4
+ * Modifications: public imports and owned editor-local typed seams; see docs/architecture/history-editor-provenance.md.
5
+ */
6
+ /**
7
+ * Generic undo stack with clone-on-push semantics.
8
+ *
9
+ * Stores deep clones of state snapshots. Popped snapshots are returned
10
+ * directly (no re-cloning) since they are already detached.
11
+ */
12
+ export class UndoStack {
13
+ stack = [];
14
+ /** Push a deep clone of the given state onto the stack. */
15
+ push(state) {
16
+ this.stack.push(structuredClone(state));
17
+ }
18
+ /** Pop and return the most recent snapshot, or undefined if empty. */
19
+ pop() {
20
+ return this.stack.pop();
21
+ }
22
+ /** Remove all snapshots. */
23
+ clear() {
24
+ this.stack.length = 0;
25
+ }
26
+ updateSnapshots(update) {
27
+ this.stack = this.stack.map(snapshot => {
28
+ const copy = structuredClone(snapshot);
29
+ update(copy);
30
+ return copy;
31
+ });
32
+ }
33
+ get length() {
34
+ return this.stack.length;
35
+ }
36
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Options for word navigation functions.
3
+ * When omitted, uses the default Intl.Segmenter word segmentation.
4
+ */
5
+ export interface WordNavigationOptions {
6
+ /** Custom segmenter returning word segments for the given text. */
7
+ segment?: (text: string) => Iterable<Intl.SegmentData>;
8
+ /** Predicate identifying atomic segments that should be treated as single units (e.g. paste markers). */
9
+ isAtomicSegment?: (segment: string) => boolean;
10
+ }
11
+ /**
12
+ * Find the cursor position after moving one word backward from `cursor` in `text`.
13
+ * Skips trailing whitespace, then stops at the next word/punctuation boundary.
14
+ *
15
+ * Pure function - does not mutate any state.
16
+ */
17
+ export declare function findWordBackward(text: string, cursor: number, options?: WordNavigationOptions): number;
18
+ /**
19
+ * Find the cursor position after moving one word forward from `cursor` in `text`.
20
+ * Skips leading whitespace, then stops at the next word/punctuation boundary.
21
+ *
22
+ * Pure function - does not mutate any state.
23
+ */
24
+ export declare function findWordForward(text: string, cursor: number, options?: WordNavigationOptions): number;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Adapted from @earendil-works/pi-tui 0.84.2, packages/tui/src/word-navigation.ts (MIT).
3
+ * Source commit: 914cf1472e715297caa30db4b9535d534a9eb718.
4
+ * Modifications: public imports and owned editor-local typed seams; see docs/architecture/history-editor-provenance.md.
5
+ */
6
+ import { getWordSegmenter, isWhitespaceChar, PUNCTUATION_REGEX } from "./text-helpers.js";
7
+ const wordSegmenter = getWordSegmenter();
8
+ /**
9
+ * Find the cursor position after moving one word backward from `cursor` in `text`.
10
+ * Skips trailing whitespace, then stops at the next word/punctuation boundary.
11
+ *
12
+ * Pure function - does not mutate any state.
13
+ */
14
+ export function findWordBackward(text, cursor, options) {
15
+ if (cursor <= 0)
16
+ return 0;
17
+ const textBeforeCursor = text.slice(0, cursor);
18
+ const segmentFn = options?.segment;
19
+ const isAtomic = options?.isAtomicSegment;
20
+ const segments = segmentFn ? [...segmentFn(textBeforeCursor)] : [...wordSegmenter.segment(textBeforeCursor)];
21
+ let newCursor = cursor;
22
+ // Skip trailing whitespace
23
+ while (segments.length > 0 &&
24
+ !isAtomic?.(segments[segments.length - 1]?.segment || "") &&
25
+ isWhitespaceChar(segments[segments.length - 1]?.segment || "")) {
26
+ newCursor -= segments.pop()?.segment.length || 0;
27
+ }
28
+ if (segments.length === 0)
29
+ return newCursor;
30
+ const last = segments[segments.length - 1];
31
+ if (isAtomic?.(last.segment)) {
32
+ // Skip one atomic segment.
33
+ newCursor -= last.segment.length;
34
+ }
35
+ else if (last.isWordLike) {
36
+ // Skip inside one word-like segment, preserving ASCII punctuation boundaries.
37
+ const segment = last.segment;
38
+ const matches = [...segment.matchAll(new RegExp(PUNCTUATION_REGEX, "g"))];
39
+ if (matches.length <= 0) {
40
+ newCursor -= segment.length;
41
+ }
42
+ else {
43
+ const lastMatch = matches[matches.length - 1];
44
+ newCursor -= segment.length - (lastMatch.index + lastMatch[0].length);
45
+ }
46
+ }
47
+ else {
48
+ // Skip non-word non-whitespace run (punctuation)
49
+ while (segments.length > 0 &&
50
+ !isAtomic?.(segments[segments.length - 1]?.segment || "") &&
51
+ !segments[segments.length - 1]?.isWordLike &&
52
+ !isWhitespaceChar(segments[segments.length - 1]?.segment || "")) {
53
+ newCursor -= segments.pop()?.segment.length || 0;
54
+ }
55
+ }
56
+ return newCursor;
57
+ }
58
+ /**
59
+ * Find the cursor position after moving one word forward from `cursor` in `text`.
60
+ * Skips leading whitespace, then stops at the next word/punctuation boundary.
61
+ *
62
+ * Pure function - does not mutate any state.
63
+ */
64
+ export function findWordForward(text, cursor, options) {
65
+ if (cursor >= text.length)
66
+ return text.length;
67
+ const textAfterCursor = text.slice(cursor);
68
+ const segmentFn = options?.segment;
69
+ const isAtomic = options?.isAtomicSegment;
70
+ const segments = segmentFn ? segmentFn(textAfterCursor) : wordSegmenter.segment(textAfterCursor);
71
+ const iterator = segments[Symbol.iterator]();
72
+ let next = iterator.next();
73
+ let newCursor = cursor;
74
+ // Skip leading whitespace
75
+ while (!next.done && !isAtomic?.(next.value.segment) && isWhitespaceChar(next.value.segment)) {
76
+ newCursor += next.value.segment.length;
77
+ next = iterator.next();
78
+ }
79
+ if (next.done)
80
+ return newCursor;
81
+ if (isAtomic?.(next.value.segment)) {
82
+ // Skip one atomic segment.
83
+ newCursor += next.value.segment.length;
84
+ }
85
+ else if (next.value.isWordLike) {
86
+ // Skip inside one word-like segment, preserving ASCII punctuation boundaries.
87
+ newCursor += PUNCTUATION_REGEX.exec(next.value.segment)?.index ?? next.value.segment.length;
88
+ }
89
+ else {
90
+ // Skip non-word non-whitespace run (punctuation)
91
+ while (!next.done &&
92
+ !isAtomic?.(next.value.segment) &&
93
+ !next.value.isWordLike &&
94
+ !isWhitespaceChar(next.value.segment)) {
95
+ newCursor += next.value.segment.length;
96
+ next = iterator.next();
97
+ }
98
+ }
99
+ return newCursor;
100
+ }
@@ -28,5 +28,6 @@ export declare class PromptChipStore {
28
28
  target: string;
29
29
  }[];
30
30
  expandCopiedText(text: string): string;
31
+ prepareHistoryText(text: string): string;
31
32
  prepareSubmission(text: string): PreparedPrompt;
32
33
  }
@@ -171,6 +171,9 @@ export class PromptChipStore {
171
171
  expandCopiedText(text) {
172
172
  return this.#replaceResolvable(text, false).text;
173
173
  }
174
+ prepareHistoryText(text) {
175
+ return this.#replaceResolvable(text, false).text.replace(CHIP_PATTERN, tag => this.#chips.get(tag)?.kind === "image" ? "" : tag).trim();
176
+ }
174
177
  prepareSubmission(text) {
175
178
  const expanded = this.#replaceResolvable(text, true);
176
179
  return { text: expanded.text, images: expanded.images };
@@ -0,0 +1,30 @@
1
+ import { type PromptHistoryKind, type PromptHistoryPort } from "../../../contracts/owned-ui/index.js";
2
+ import type { PiShellEditorPort } from "../components/index.js";
3
+ /** Merges bounded recall snapshots; the editor alone owns browsing, draft, cursor, and undo state. */
4
+ export declare class PromptHistoryController {
5
+ #private;
6
+ readonly options: {
7
+ editor: PiShellEditorPort;
8
+ store: PromptHistoryPort;
9
+ limit: number;
10
+ fallback: readonly string[];
11
+ active(): boolean;
12
+ render(): void;
13
+ failure(message: string): void;
14
+ };
15
+ constructor(options: {
16
+ editor: PiShellEditorPort;
17
+ store: PromptHistoryPort;
18
+ limit: number;
19
+ fallback: readonly string[];
20
+ active(): boolean;
21
+ render(): void;
22
+ failure(message: string): void;
23
+ });
24
+ start(): void;
25
+ capture(text: string, kind: PromptHistoryKind, cwd: string, sessionId: string): void;
26
+ rememberRecovery(text: string): void;
27
+ synchronize(): void;
28
+ reset(fallback: readonly string[]): void;
29
+ close(): Promise<boolean>;
30
+ }
@@ -0,0 +1,147 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { PROMPT_HISTORY_MAX_TEXT_BYTES } from "../../../contracts/owned-ui/index.js";
3
+ /** Merges bounded recall snapshots; the editor alone owns browsing, draft, cursor, and undo state. */
4
+ export class PromptHistoryController {
5
+ options;
6
+ #local = new Map();
7
+ #unsubscribe;
8
+ #snapshot;
9
+ #fallback;
10
+ #disposed = false;
11
+ #generation = 0;
12
+ #unsubscribeSnapshot;
13
+ #unsubscribeFailure;
14
+ #closeFailed = false;
15
+ #closePromise;
16
+ constructor(options) {
17
+ this.options = options;
18
+ if (options.editor.recall === undefined)
19
+ throw new Error("The selected editor does not expose typed history");
20
+ this.#snapshot = { revision: 0, limit: options.limit, entries: [] };
21
+ this.#fallback = boundedTexts([...options.fallback].reverse(), options.limit);
22
+ this.#unsubscribeSnapshot = this.#subscribeSnapshot();
23
+ this.#unsubscribeFailure = options.store.onFailure(code => {
24
+ if (this.#disposed)
25
+ this.#closeFailed = true;
26
+ else
27
+ options.failure(`Prompt history ${code}; current-session recall remains available.`);
28
+ });
29
+ this.#unsubscribe = [
30
+ options.editor.recall.observe(() => {
31
+ if (this.#disposed)
32
+ return;
33
+ if (options.editor.recall.position().index === -1)
34
+ this.synchronize();
35
+ options.store.refresh();
36
+ }),
37
+ ];
38
+ this.synchronize();
39
+ }
40
+ start() { this.options.store.start(); }
41
+ capture(text, kind, cwd, sessionId) {
42
+ const canonical = text.trim();
43
+ if (!canonical || this.#disposed)
44
+ return;
45
+ const id = randomUUID();
46
+ const generation = this.#generation;
47
+ this.#local.set(id, { text: canonical, committed: false });
48
+ this.#boundLocal();
49
+ this.synchronize();
50
+ void this.options.store.record({ id, text: canonical, timestamp: Date.now(), kind, cwd, sessionId }).then(outcome => {
51
+ if (this.#disposed || generation !== this.#generation)
52
+ return;
53
+ const local = this.#local.get(id);
54
+ if (local === undefined || outcome !== "committed")
55
+ return;
56
+ local.committed = true;
57
+ if (this.#snapshot.entries.some(entry => entry.submissionId === id))
58
+ this.#local.delete(id);
59
+ this.synchronize();
60
+ }).catch(() => {
61
+ if (!this.#disposed)
62
+ this.options.failure("Prompt history unavailable; current-session recall remains available.");
63
+ });
64
+ }
65
+ rememberRecovery(text) {
66
+ if (this.#disposed || !text.trim())
67
+ return;
68
+ const canonical = text.trim();
69
+ if ([...this.#local.values()].some(item => item.text === canonical)
70
+ || this.#snapshot.entries.some(item => item.text === canonical))
71
+ return;
72
+ this.#local.set(randomUUID(), { text: canonical, committed: false });
73
+ this.#boundLocal();
74
+ this.synchronize();
75
+ }
76
+ synchronize() {
77
+ if (this.#disposed || !this.options.active())
78
+ return;
79
+ const values = [...this.#local.values()].reverse().map(item => item.text)
80
+ .concat(this.#snapshot.entries.map(item => item.text), this.#fallback);
81
+ this.options.editor.recall.replace(boundedTexts(values, this.#snapshot.limit));
82
+ this.options.render();
83
+ }
84
+ reset(fallback) {
85
+ this.#generation++;
86
+ this.#unsubscribeSnapshot();
87
+ this.#unsubscribeSnapshot = this.#subscribeSnapshot();
88
+ this.#fallback = boundedTexts([...fallback].reverse(), this.options.limit);
89
+ this.#local.clear();
90
+ this.options.editor.recall.reset();
91
+ this.synchronize();
92
+ this.options.store.refresh();
93
+ }
94
+ close() {
95
+ if (this.#closePromise !== undefined)
96
+ return this.#closePromise;
97
+ this.#disposed = true;
98
+ this.#unsubscribeSnapshot();
99
+ for (const unsubscribe of this.#unsubscribe)
100
+ unsubscribe();
101
+ this.#local.clear();
102
+ this.#fallback = [];
103
+ this.#closePromise = this.options.store.close().then(() => !this.#closeFailed, () => false)
104
+ .finally(() => this.#unsubscribeFailure());
105
+ return this.#closePromise;
106
+ }
107
+ #subscribeSnapshot() {
108
+ const generation = this.#generation;
109
+ return this.options.store.onSnapshot(snapshot => {
110
+ if (this.#disposed || generation !== this.#generation || snapshot.revision < this.#snapshot.revision)
111
+ return;
112
+ this.#snapshot = snapshot;
113
+ for (const [id, entry] of this.#local) {
114
+ if (entry.committed)
115
+ this.#local.delete(id);
116
+ }
117
+ this.synchronize();
118
+ });
119
+ }
120
+ #boundLocal() {
121
+ let bytes = [...this.#local.values()].reduce((sum, item) => sum + Buffer.byteLength(item.text), 0);
122
+ while (this.#local.size > this.options.limit || bytes > PROMPT_HISTORY_MAX_TEXT_BYTES) {
123
+ const id = this.#local.keys().next().value;
124
+ bytes -= Buffer.byteLength(this.#local.get(id).text);
125
+ this.#local.delete(id);
126
+ }
127
+ }
128
+ }
129
+ function boundedTexts(values, limit) {
130
+ const result = [];
131
+ const seen = new Set();
132
+ let bytes = 0;
133
+ for (const value of values) {
134
+ const text = value.trim();
135
+ if (!text || seen.has(text))
136
+ continue;
137
+ const size = Buffer.byteLength(text);
138
+ if (bytes + size > PROMPT_HISTORY_MAX_TEXT_BYTES)
139
+ continue;
140
+ result.push(text);
141
+ seen.add(text);
142
+ bytes += size;
143
+ if (result.length >= limit)
144
+ break;
145
+ }
146
+ return result;
147
+ }
@@ -18,6 +18,11 @@ export interface OwnedUiClipboardPort {
18
18
  writeText?(text: string): Promise<void>;
19
19
  }
20
20
  export interface OwnedUiSessionShellOptions {
21
+ readonly promptHistory?: {
22
+ readonly store: import("../../../contracts/owned-ui/index.js").PromptHistoryPort;
23
+ readonly limit: number;
24
+ readonly editor: import("../components/index.js").HistoryEditorConstructor;
25
+ };
21
26
  readonly backend: OwnedUiBackendPort;
22
27
  readonly cwd: string;
23
28
  readonly terminal?: OwnedUiTerminalPort;
@@ -67,6 +72,8 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
67
72
  readonly onViewportFrame?: (frame: TranscriptViewportFrame) => void;
68
73
  readonly requestHyperlinkCleanup?: () => void;
69
74
  readonly enableDockInputReuse?: boolean;
75
+ readonly persistentHistory?: boolean;
76
+ readonly historyEditor?: import("../components/index.js").HistoryEditorConstructor;
70
77
  readonly onSubmit: (text: string) => void;
71
78
  readonly onPasteRejected?: (error: unknown) => void;
72
79
  readonly onInterrupt: () => void;
@@ -91,6 +98,7 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
91
98
  setHideThinkingBlock(hidden: boolean): void;
92
99
  setMermaidRenderingMode(mode: "off" | "final" | "streaming"): void;
93
100
  setImagePresentation(showImages: boolean, imageWidthCells: number): void;
101
+ prepareHistoryText(text: string): string;
94
102
  preparePromptSubmission(text: string): PreparedPrompt;
95
103
  hasPendingPastes(text: string): boolean;
96
104
  waitForPromptPastes(text: string, signal: AbortSignal): Promise<PreparedPrompt>;
@@ -204,6 +204,9 @@ export class OwnedUiSessionShellRoot {
204
204
  component.setImagePresentation(showImages, imageWidthCells);
205
205
  this.#invalidateTranscriptPresentation();
206
206
  }
207
+ prepareHistoryText(text) {
208
+ return this.#promptChips.prepareHistoryText(text);
209
+ }
207
210
  preparePromptSubmission(text) {
208
211
  return this.#promptChips.prepareSubmission(text);
209
212
  }