@timurproko/a1 0.1.8-dev.290 → 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 (62) 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 +1 -1
  49. package/dist/native/linux-x64/manifest.json +1 -1
  50. package/dist/native/win32-x64/manifest.json +2 -2
  51. package/dist/native/win32-x64/process-guardian.exe +0 -0
  52. package/dist/product-identity.d.ts +1 -1
  53. package/dist/product-identity.js +1 -1
  54. package/dist/product-identity.json +1 -0
  55. package/dist/ui/settings/declarations.js +18 -0
  56. package/dist/ui/settings/session.js +13 -4
  57. package/docs/architecture/boundaries.md +1 -0
  58. package/docs/architecture/history-editor-provenance.md +96 -0
  59. package/docs/architecture/project-structure.md +1 -0
  60. package/docs/architecture/resource-and-data-policy.md +22 -1
  61. package/docs/features/prompt-history.md +121 -0
  62. package/package.json +1 -1
@@ -0,0 +1,42 @@
1
+ import { parentPort, workerData } from "node:worker_threads";
2
+ import { setTimeout as delay } from "node:timers/promises";
3
+ import { PromptHistoryStore, classifyHistoryError } from "./store.js";
4
+ const port = parentPort;
5
+ if (port !== null) {
6
+ let store;
7
+ let chain = Promise.resolve();
8
+ const options = workerData;
9
+ const open = retry(() => { store = new PromptHistoryStore(options.path, options.profileId, options.limit); });
10
+ void open.then(() => port.postMessage({ id: 0, ok: true }), error => port.postMessage({ id: 0, ok: false, code: classifyHistoryError(error) }));
11
+ port.on("message", (request) => {
12
+ chain = chain.then(async () => {
13
+ try {
14
+ await open;
15
+ if (request.kind === "close") {
16
+ store.close();
17
+ port.postMessage({ id: request.id, ok: true });
18
+ port.close();
19
+ return;
20
+ }
21
+ const value = await retry(() => request.kind === "record" ? store.record(request.submission) : store.snapshot());
22
+ port.postMessage({ id: request.id, ok: true, value });
23
+ }
24
+ catch (error) {
25
+ port.postMessage({ id: request.id, ok: false, code: classifyHistoryError(error) });
26
+ }
27
+ });
28
+ });
29
+ }
30
+ async function retry(operation) {
31
+ const deadline = performance.now() + 1000;
32
+ for (;;) {
33
+ try {
34
+ return operation();
35
+ }
36
+ catch (error) {
37
+ if (classifyHistoryError(error) !== "busy" || performance.now() >= deadline)
38
+ throw error;
39
+ await delay(25);
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,38 @@
1
+ import type { Editor } from "#pi-tui";
2
+ export interface EditorInteractionState {
3
+ lines: string[];
4
+ cursorLine: number;
5
+ cursorCol: number;
6
+ }
7
+ export interface EditorInteractionPort {
8
+ snapshot(): EditorInteractionState;
9
+ setCursor(line: number, col: number): void;
10
+ replaceLines(lines: readonly string[]): void;
11
+ updateUndoStates(update: (state: EditorInteractionState) => void): void;
12
+ scrollOffset(): number;
13
+ visualLines(width: number): Array<{
14
+ logicalLine: number;
15
+ startCol: number;
16
+ length: number;
17
+ }>;
18
+ setSegmentTransform(transform: (text: string, mode: "word" | "grapheme", segments: Iterable<Intl.SegmentData>) => Iterable<Intl.SegmentData>): void;
19
+ }
20
+ export interface EditorRecallPort {
21
+ replace(entries: readonly string[]): void;
22
+ position(): {
23
+ readonly index: number;
24
+ readonly total: number;
25
+ };
26
+ observe(listener: () => void): () => void;
27
+ reset(): void;
28
+ }
29
+ export interface EditorSurface extends Pick<Editor, Exclude<keyof Editor, "setText" | "render" | "handleInput">> {
30
+ setText(text: string): void;
31
+ render(width: number): string[];
32
+ handleInput(data: string): void;
33
+ readonly interaction?: EditorInteractionPort;
34
+ readonly recall?: EditorRecallPort;
35
+ }
36
+ export type SelectionEditor = Pick<Editor, "getText" | "getCursor" | "setText" | "insertTextAtCursor" | "invalidate" | "onChange" | "getPaddingX" | "isShowingAutocomplete"> & {
37
+ readonly interaction?: EditorInteractionPort;
38
+ };
@@ -0,0 +1,5 @@
1
+ import { type ShellEditorInstance, type OwnedEditorOptions } from "./upstream/components/owned-editor.js";
2
+ import type { EditorTheme, TUI } from "#pi-tui";
3
+ import type { KeybindingsManager } from "./upstream/adjacent/core/keybindings.js";
4
+ export type HistoryEditorConstructor = new (tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager, options?: OwnedEditorOptions) => ShellEditorInstance;
5
+ export declare function loadHistoryEditor(): Promise<HistoryEditorConstructor>;
@@ -0,0 +1,5 @@
1
+ import { createOwnedEditorClass } from "./upstream/components/owned-editor.js";
2
+ export async function loadHistoryEditor() {
3
+ const { HistoryEditorCore } = await import("./upstream/history/editor-core.js");
4
+ return createOwnedEditorClass(HistoryEditorCore);
5
+ }
@@ -1,4 +1,5 @@
1
1
  export * from "./components.js";
2
+ export { loadHistoryEditor, type HistoryEditorConstructor } from "./history-editor-loader.js";
2
3
  export * from "./conformance.js";
3
4
  export * from "./shell-components.js";
4
5
  export * from "./theme.js";
@@ -1,4 +1,5 @@
1
1
  export * from "./components.js";
2
+ export { loadHistoryEditor } from "./history-editor-loader.js";
2
3
  export * from "./conformance.js";
3
4
  export * from "./shell-components.js";
4
5
  export * from "./theme.js";
@@ -1,4 +1,4 @@
1
- import { type Editor } from "#pi-tui";
1
+ import type { SelectionEditor as Editor } from "./editor-interaction.js";
2
2
  import type { KeybindingsManager } from "./upstream/adjacent/core/keybindings.js";
3
3
  import type { PiShellClipboardContent, PiShellEditorTextRange } from "./shell-shared-facade.js";
4
4
  /**
@@ -211,7 +211,7 @@ class PromptSelectionInterceptor {
211
211
  const layoutWidth = Math.max(1, contentWidth - (padding ? 0 : 1));
212
212
  const visualLines = editorVisualLineMap(this.editor, layoutWidth)
213
213
  ?? buildVisualLineMap(editorState(this.editor).lines, layoutWidth);
214
- const scrollOffset = numericProperty(this.editor, "scrollOffset");
214
+ const scrollOffset = this.editor.interaction?.scrollOffset() ?? numericProperty(this.editor, "scrollOffset");
215
215
  const maxVisibleLines = Math.max(5, Math.floor(this.options.getRows() * 0.3));
216
216
  const textRows = Math.max(0, Math.min(visualLines.length - scrollOffset, maxVisibleLines, rows.length - 2));
217
217
  this.#geometry = { width, padding, layoutWidth, visualLines, scrollOffset, textRows };
@@ -464,15 +464,21 @@ class PromptSelectionInterceptor {
464
464
  const selection = this.#selection === undefined ? undefined : { anchor: remap(this.#selection.anchor), head: remap(this.#selection.head) };
465
465
  const next = current.slice(0, from) + text + current.slice(to);
466
466
  // Compatibility: completing a paste updates its provisional undo snapshots, not the user's undo history.
467
- editorState(this.editor).lines = next.split("\n");
468
- const undo = Reflect.get(this.editor, "undoStack");
469
- const snapshots = typeof undo === "object" && undo !== null ? Reflect.get(undo, "stack") : undefined;
470
- if (Array.isArray(snapshots))
471
- for (const snapshot of snapshots) {
472
- const state = typeof snapshot === "object" && snapshot !== null ? Reflect.get(snapshot, "state") : undefined;
473
- if (isEditorState(state))
474
- replaceSnapshotMarker(state, marker, text);
475
- }
467
+ if (this.editor.interaction !== undefined) {
468
+ this.editor.interaction.replaceLines(next.split("\n"));
469
+ this.editor.interaction.updateUndoStates(state => replaceSnapshotMarker(state, marker, text));
470
+ }
471
+ else {
472
+ editorState(this.editor).lines = next.split("\n");
473
+ const undo = Reflect.get(this.editor, "undoStack");
474
+ const snapshots = typeof undo === "object" && undo !== null ? Reflect.get(undo, "stack") : undefined;
475
+ if (Array.isArray(snapshots))
476
+ for (const snapshot of snapshots) {
477
+ const state = typeof snapshot === "object" && snapshot !== null ? Reflect.get(snapshot, "state") : undefined;
478
+ if (isEditorState(state))
479
+ replaceSnapshotMarker(state, marker, text);
480
+ }
481
+ }
476
482
  for (const snapshot of this.#redoStack) {
477
483
  const state = { lines: snapshot.text.split("\n"), cursorLine: snapshot.cursor.line, cursorCol: snapshot.cursor.col };
478
484
  replaceSnapshotMarker(state, marker, text);
@@ -602,6 +608,10 @@ class PromptSelectionInterceptor {
602
608
  return this.editor.getCursor();
603
609
  }
604
610
  #setCursor(position) {
611
+ if (this.editor.interaction !== undefined) {
612
+ this.editor.interaction.setCursor(position.line, position.col);
613
+ return;
614
+ }
605
615
  const state = editorState(this.editor);
606
616
  const line = clamp(position.line, 0, Math.max(0, state.lines.length - 1));
607
617
  state.cursorLine = line;
@@ -615,6 +625,7 @@ class PromptSelectionInterceptor {
615
625
  : state.cursorCol >= range.start && state.cursorCol < range.end);
616
626
  if (atomic !== undefined) {
617
627
  state.cursorCol = delta < 0 ? atomic.start : atomic.end;
628
+ this.editor.interaction?.setCursor(state.cursorLine, state.cursorCol);
618
629
  return;
619
630
  }
620
631
  if (delta < 0) {
@@ -626,6 +637,7 @@ class PromptSelectionInterceptor {
626
637
  state.cursorLine -= 1;
627
638
  state.cursorCol = (state.lines[state.cursorLine] ?? "").length;
628
639
  }
640
+ this.editor.interaction?.setCursor(state.cursorLine, state.cursorCol);
629
641
  return;
630
642
  }
631
643
  if (state.cursorCol < line.length) {
@@ -635,6 +647,7 @@ class PromptSelectionInterceptor {
635
647
  state.cursorLine += 1;
636
648
  state.cursorCol = 0;
637
649
  }
650
+ this.editor.interaction?.setCursor(state.cursorLine, state.cursorCol);
638
651
  }
639
652
  #atomicRangeAt(position) {
640
653
  const line = editorState(this.editor).lines[position.line] ?? "";
@@ -674,14 +687,8 @@ class PromptSelectionInterceptor {
674
687
  }
675
688
  }
676
689
  function installAtomicSegmentation(editor, rangesForText, wordDirection) {
677
- if (Reflect.get(editor, ATOMIC_SEGMENTATION) === true)
678
- return;
679
- const originalValue = Reflect.get(editor, "segment");
680
- if (typeof originalValue !== "function")
681
- return;
682
- const original = originalValue.bind(editor);
683
- Reflect.set(editor, "segment", (text, mode) => {
684
- const segments = [...original(text, mode)].filter(isEditorSegment);
690
+ const transform = (text, mode, values) => {
691
+ const segments = [...values].filter(isEditorSegment);
685
692
  const ranges = rangesForText(text).map(range => ({ ...range, wordLike: false }));
686
693
  if (mode === "word") {
687
694
  for (const range of contextualPathRanges(editor, text, wordDirection())) {
@@ -713,7 +720,18 @@ function installAtomicSegmentation(editor, rangesForText, wordDirection) {
713
720
  merged.push(segment);
714
721
  }
715
722
  return merged;
716
- });
723
+ };
724
+ if (editor.interaction !== undefined) {
725
+ editor.interaction.setSegmentTransform(transform);
726
+ return;
727
+ }
728
+ if (Reflect.get(editor, ATOMIC_SEGMENTATION) === true)
729
+ return;
730
+ const originalValue = Reflect.get(editor, "segment");
731
+ if (typeof originalValue !== "function")
732
+ return;
733
+ const original = originalValue.bind(editor);
734
+ Reflect.set(editor, "segment", (text, mode) => transform(text, mode, original(text, mode)));
717
735
  Reflect.set(editor, ATOMIC_SEGMENTATION, true);
718
736
  }
719
737
  function contextualPathRanges(editor, text, direction) {
@@ -787,6 +805,8 @@ function sameSnapshot(left, right) {
787
805
  return left.text === right.text && samePosition(left.cursor, right.cursor);
788
806
  }
789
807
  function editorState(editor) {
808
+ if (editor.interaction !== undefined)
809
+ return editor.interaction.snapshot();
790
810
  const value = Object.getOwnPropertyDescriptor(editor, "state")?.value;
791
811
  if (!isEditorState(value))
792
812
  throw new Error("Pi editor state is unavailable");
@@ -806,6 +826,8 @@ function numericProperty(target, key) {
806
826
  return typeof value === "number" ? value : 0;
807
827
  }
808
828
  function editorVisualLineMap(editor, width) {
829
+ if (editor.interaction !== undefined)
830
+ return editor.interaction.visualLines(width);
809
831
  const builder = Reflect.get(editor, "buildVisualLineMap");
810
832
  if (typeof builder !== "function")
811
833
  return undefined;
@@ -1,5 +1,6 @@
1
1
  import { getSelectListTheme } from "@earendil-works/pi-coding-agent";
2
2
  import { CombinedAutocompleteProvider, matchesKey, setKeybindings, } from "#pi-tui";
3
+ import { PROMPT_HISTORY_EDITOR_REPLACEMENT } from "../../../contracts/owned-ui/index.js";
3
4
  import { KeybindingsManager } from "./upstream/adjacent/core/keybindings.js";
4
5
  import { OwnedEditor } from "./upstream/components/owned-editor.js";
5
6
  import { OwnedEditorUxInterception, createPromptSelectionInterceptor, } from "./owned-editor-ux.js";
@@ -36,12 +37,18 @@ export function createPiShellEditor(options) {
36
37
  ? KeybindingsManager.createForOwnedInput(options.agentDir)
37
38
  : KeybindingsManager.create(options.agentDir);
38
39
  setKeybindings(keybindings);
39
- const editor = new OwnedEditor(tui, {
40
+ if (options.persistentHistory === true && options.keybindingProfile === "a1" && options.historyEditor === undefined) {
41
+ throw new Error("Persistent history requires the loaded owned editor");
42
+ }
43
+ const EditorClass = options.keybindingProfile === "a1" && options.persistentHistory === true ? options.historyEditor : OwnedEditor;
44
+ const editor = new EditorClass(tui, {
40
45
  borderColor: (value) => piTheme().fg("borderMuted", value),
41
46
  selectList: getSelectListTheme(),
42
47
  }, keybindings, {
43
48
  paddingX: PINNED_PI_LAYOUT.editorPaddingX,
44
49
  autocompleteMaxVisible: PINNED_PI_LAYOUT.autocompleteMaxVisible,
50
+ persistentHistory: options.persistentHistory === true,
51
+ styleHistoryLabel: text => piTheme().fg("dim", text),
45
52
  ...(options.keybindingProfile === "a1" && options.promptPresentation !== undefined ? {
46
53
  promptPrefix: options.promptPresentation.prefix,
47
54
  styleSuggestion: options.promptPresentation.styleSuggestion,
@@ -163,6 +170,7 @@ export function createPiShellEditor(options) {
163
170
  editor.insertTextAtCursor(text);
164
171
  },
165
172
  addToHistory: text => editor.addToHistory(text),
173
+ ...(editor.recall === undefined ? {} : { recall: editor.recall, historyReplacement: PROMPT_HISTORY_EDITOR_REPLACEMENT }),
166
174
  setSubmitEnabled: enabled => {
167
175
  editor.disableSubmit = !enabled;
168
176
  },
@@ -1,5 +1,7 @@
1
1
  import { visibleWidth, type AutocompleteProvider, type Component, type TUI } from "#pi-tui";
2
2
  import type { OwnedUiImageAttachment, OwnedUiSessionViewModel, OwnedUiThinkingLevel, OwnedUiTranscriptBlock } from "../../../contracts/owned-ui/index.js";
3
+ import type { HistoryEditorConstructor } from "./history-editor-loader.js";
4
+ import type { EditorRecallPort } from "./editor-interaction.js";
3
5
  import type { PresentationComponentPort } from "../../../contracts/presentation/index.js";
4
6
  /** Terminal-cell width authority used by Pi-rendered component rows. */
5
7
  export declare const piShellVisibleWidth: typeof visibleWidth;
@@ -25,6 +27,8 @@ export interface PiShellEditorPointerEvent {
25
27
  readonly row: number;
26
28
  }
27
29
  export interface PiShellEditorPort extends PiShellComponentPort {
30
+ readonly recall?: EditorRecallPort;
31
+ readonly historyReplacement?: typeof import("../../../contracts/owned-ui/index.js").PROMPT_HISTORY_EDITOR_REPLACEMENT;
28
32
  /** Restores this editor's profile after another Pi component changed the global manager. */
29
33
  activateKeybindings(): void;
30
34
  /** Uses Pi's terminal decoder rather than assuming one terminal escape spelling. */
@@ -112,6 +116,8 @@ export interface PiShellHeaderOptions {
112
116
  readonly resources?: readonly PiShellResourceEntry[];
113
117
  }
114
118
  export interface PiShellEditorOptions {
119
+ readonly persistentHistory?: boolean;
120
+ readonly historyEditor?: HistoryEditorConstructor;
115
121
  /** Bare A1 adds ergonomic aliases while comparison profiles retain Pi defaults. */
116
122
  readonly keybindingProfile?: "pi" | "a1";
117
123
  readonly getColumns: () => number;
@@ -4,28 +4,32 @@
4
4
  * Modifications: A1-owned class name, synchronized keybinding contract, and a semantic
5
5
  * bare-A1 prompt-prefix/contextual-suggestion presentation branch.
6
6
  */
7
- import { Editor, type EditorOptions, type EditorTheme, type TUI } from "#pi-tui";
7
+ import { type EditorOptions, type EditorTheme, type TUI } from "#pi-tui";
8
8
  import type { AppKeybinding, KeybindingsManager } from "../adjacent/core/keybindings.js";
9
+ import type { EditorSurface } from "../../editor-interaction.js";
9
10
  export interface OwnedEditorOptions extends EditorOptions {
11
+ readonly persistentHistory?: boolean;
12
+ readonly styleHistoryLabel?: (text: string) => string;
10
13
  readonly promptPrefix?: string;
11
14
  readonly styleSuggestion?: (text: string) => string;
12
15
  readonly styleSuggestionCaret?: (text: string) => string;
13
16
  readonly terminalRows?: () => number;
14
17
  }
15
- export declare class OwnedEditor extends Editor {
16
- #private;
17
- private readonly keybindings;
18
- readonly actionHandlers: Map<keyof import("../adjacent/core/keybindings.js").AppKeybindings, () => void>;
18
+ export interface ShellEditorInstance extends EditorSurface {
19
+ readonly actionHandlers: Map<AppKeybinding, () => void>;
19
20
  onEscape?: () => void;
20
21
  onCtrlD?: () => void;
21
22
  onPasteImage?: () => void;
22
23
  onExtensionShortcut?: (data: string) => boolean;
23
24
  onPromptSuggestionAccepted?: (text: string) => void;
24
- constructor(tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager, options?: OwnedEditorOptions);
25
25
  setPromptSuggestion(text: string | null): void;
26
26
  canPresentPromptSuggestion(): boolean;
27
- setText(text: string): void;
28
- render(width: number): string[];
29
27
  onAction(action: AppKeybinding, handler: () => void): void;
30
- handleInput(data: string): void;
31
28
  }
29
+ type EditorConstructor = new (tui: TUI, theme: EditorTheme, options?: EditorOptions) => EditorSurface;
30
+ type ShellEditorConstructor = new (tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager, options?: OwnedEditorOptions) => ShellEditorInstance;
31
+ export declare function createOwnedEditorClass(Base: EditorConstructor): ShellEditorConstructor;
32
+ declare const OwnedEditor_base: ShellEditorConstructor;
33
+ export declare class OwnedEditor extends OwnedEditor_base {
34
+ }
35
+ export {};
@@ -6,149 +6,153 @@
6
6
  */
7
7
  import { CURSOR_MARKER, Editor, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "#pi-tui";
8
8
  const PROMPT_GRAPHEMES = new Intl.Segmenter(undefined, { granularity: "grapheme" });
9
- export class OwnedEditor extends Editor {
10
- keybindings;
11
- actionHandlers = new Map();
12
- onEscape;
13
- onCtrlD;
14
- onPasteImage;
15
- onExtensionShortcut;
16
- onPromptSuggestionAccepted;
17
- #promptSuggestion = null;
18
- #promptPrefix;
19
- #styleSuggestion;
20
- #styleSuggestionCaret;
21
- #terminalRows;
22
- constructor(tui, theme, keybindings, options = {}) {
23
- super(tui, theme, options);
24
- this.keybindings = keybindings;
25
- this.#promptPrefix = options.promptPrefix ?? "";
26
- this.#styleSuggestion = options.styleSuggestion ?? (text => text);
27
- this.#styleSuggestionCaret = options.styleSuggestionCaret ?? (text => `\u001b[7m${text}\u001b[27m`);
28
- this.#terminalRows = options.terminalRows ?? (() => 24);
29
- }
30
- setPromptSuggestion(text) {
31
- this.#promptSuggestion = text;
32
- }
33
- canPresentPromptSuggestion() {
34
- return this.#promptPrefix.length > 0
35
- && this.focused
36
- && !this.disableSubmit
37
- && this.getText().length === 0
38
- && !this.isShowingAutocomplete();
39
- }
40
- setText(text) {
41
- if (text.length > 0)
42
- this.#promptSuggestion = null;
43
- super.setText(text);
44
- }
45
- render(width) {
46
- if (this.#promptPrefix.length === 0)
47
- return super.render(width);
48
- if (this.#promptSuggestion !== null && this.canPresentPromptSuggestion()) {
49
- return this.#renderSuggestion(width);
9
+ export function createOwnedEditorClass(Base) {
10
+ return class extends Base {
11
+ keybindings;
12
+ actionHandlers = new Map();
13
+ onEscape;
14
+ onCtrlD;
15
+ onPasteImage;
16
+ onExtensionShortcut;
17
+ onPromptSuggestionAccepted;
18
+ #promptSuggestion = null;
19
+ #promptPrefix;
20
+ #styleSuggestion;
21
+ #styleSuggestionCaret;
22
+ #terminalRows;
23
+ constructor(tui, theme, keybindings, options = {}) {
24
+ super(tui, theme, options);
25
+ this.keybindings = keybindings;
26
+ this.#promptPrefix = options.promptPrefix ?? "";
27
+ this.#styleSuggestion = options.styleSuggestion ?? (text => text);
28
+ this.#styleSuggestionCaret = options.styleSuggestionCaret ?? (text => `\u001b[7m${text}\u001b[27m`);
29
+ this.#terminalRows = options.terminalRows ?? (() => 24);
50
30
  }
51
- return this.#renderPrefixedEditor(width);
52
- }
53
- onAction(action, handler) { this.actionHandlers.set(action, handler); }
54
- handleInput(data) {
55
- if (this.onExtensionShortcut?.(data))
56
- return;
57
- if (this.#promptSuggestion !== null
58
- && !this.isShowingAutocomplete()
59
- && this.canPresentPromptSuggestion()
60
- && this.keybindings.matches(data, "tui.input.submit")) {
61
- return;
31
+ setPromptSuggestion(text) {
32
+ this.#promptSuggestion = text;
62
33
  }
63
- if (this.#promptSuggestion !== null
64
- && !this.isShowingAutocomplete()
65
- && this.canPresentPromptSuggestion()
66
- && this.keybindings.matches(data, "tui.input.tab")) {
67
- const accepted = this.#promptSuggestion;
68
- this.#promptSuggestion = null;
69
- super.setText(accepted);
70
- this.onPromptSuggestionAccepted?.(accepted);
71
- return;
34
+ canPresentPromptSuggestion() {
35
+ return this.#promptPrefix.length > 0
36
+ && this.focused
37
+ && !this.disableSubmit
38
+ && this.getText().length === 0
39
+ && !this.isShowingAutocomplete();
72
40
  }
73
- if (this.keybindings.matches(data, "app.clipboard.pasteImage")) {
74
- this.onPasteImage?.();
75
- return;
41
+ setText(text) {
42
+ if (text.length > 0)
43
+ this.#promptSuggestion = null;
44
+ super.setText(text);
76
45
  }
77
- if (this.keybindings.matches(data, "app.interrupt")) {
78
- if (!this.isShowingAutocomplete()) {
79
- const handler = this.onEscape ?? this.actionHandlers.get("app.interrupt");
80
- if (handler) {
46
+ render(width) {
47
+ if (this.#promptPrefix.length === 0)
48
+ return super.render(width);
49
+ if (this.#promptSuggestion !== null && this.canPresentPromptSuggestion()) {
50
+ return this.#renderSuggestion(width);
51
+ }
52
+ return this.#renderPrefixedEditor(width);
53
+ }
54
+ onAction(action, handler) { this.actionHandlers.set(action, handler); }
55
+ handleInput(data) {
56
+ if (this.onExtensionShortcut?.(data))
57
+ return;
58
+ if (this.#promptSuggestion !== null
59
+ && !this.isShowingAutocomplete()
60
+ && this.canPresentPromptSuggestion()
61
+ && this.keybindings.matches(data, "tui.input.submit")) {
62
+ return;
63
+ }
64
+ if (this.#promptSuggestion !== null
65
+ && !this.isShowingAutocomplete()
66
+ && this.canPresentPromptSuggestion()
67
+ && this.keybindings.matches(data, "tui.input.tab")) {
68
+ const accepted = this.#promptSuggestion;
69
+ this.#promptSuggestion = null;
70
+ super.setText(accepted);
71
+ this.onPromptSuggestionAccepted?.(accepted);
72
+ return;
73
+ }
74
+ if (this.keybindings.matches(data, "app.clipboard.pasteImage")) {
75
+ this.onPasteImage?.();
76
+ return;
77
+ }
78
+ if (this.keybindings.matches(data, "app.interrupt")) {
79
+ if (!this.isShowingAutocomplete()) {
80
+ const handler = this.onEscape ?? this.actionHandlers.get("app.interrupt");
81
+ if (handler) {
82
+ handler();
83
+ return;
84
+ }
85
+ }
86
+ super.handleInput(data);
87
+ return;
88
+ }
89
+ if (this.keybindings.matches(data, "app.exit") && this.getText().length === 0) {
90
+ const handler = this.onCtrlD ?? this.actionHandlers.get("app.exit");
91
+ if (handler)
92
+ handler();
93
+ return;
94
+ }
95
+ if (this.keybindings.matches(data, "tui.editor.historyPrevious") || this.keybindings.matches(data, "tui.editor.historyNext")) {
96
+ super.handleInput(data);
97
+ return;
98
+ }
99
+ for (const [action, handler] of this.actionHandlers) {
100
+ if (action !== "app.interrupt" && action !== "app.exit" && this.keybindings.matches(data, action)) {
81
101
  handler();
82
102
  return;
83
103
  }
84
104
  }
85
105
  super.handleInput(data);
86
- return;
87
106
  }
88
- if (this.keybindings.matches(data, "app.exit") && this.getText().length === 0) {
89
- const handler = this.onCtrlD ?? this.actionHandlers.get("app.exit");
90
- if (handler)
91
- handler();
92
- return;
93
- }
94
- if (this.keybindings.matches(data, "tui.editor.historyPrevious") || this.keybindings.matches(data, "tui.editor.historyNext")) {
95
- super.handleInput(data);
96
- return;
107
+ #renderSuggestion(width) {
108
+ const prefixWidth = visibleWidth(this.#promptPrefix);
109
+ const innerWidth = Math.max(1, width - prefixWidth);
110
+ const maxPadding = Math.max(0, Math.floor((innerWidth - 1) / 2));
111
+ const paddingX = Math.min(this.getPaddingX(), maxPadding);
112
+ const contentWidth = Math.max(1, innerWidth - paddingX * 2);
113
+ const layoutWidth = Math.max(1, contentWidth - (paddingX ? 0 : 1));
114
+ const chunks = wrapTextWithAnsi(this.#promptSuggestion ?? "", layoutWidth).map(text => ({ text }));
115
+ const maxVisible = Math.max(5, Math.floor(this.#terminalRows() * 0.3));
116
+ const visible = chunks.slice(0, maxVisible);
117
+ const horizontal = this.borderColor("─".repeat(Math.max(0, width)));
118
+ const leftPadding = " ".repeat(paddingX);
119
+ const rightPadding = leftPadding;
120
+ const rows = visible.map((chunk, index) => {
121
+ let content;
122
+ if (index === 0) {
123
+ const first = [...PROMPT_GRAPHEMES.segment(chunk.text)][0]?.segment ?? " ";
124
+ const remaining = chunk.text.slice(first === " " && chunk.text.length === 0 ? 0 : first.length);
125
+ const marker = this.focused ? CURSOR_MARKER : "";
126
+ content = `${marker}${this.#styleSuggestionCaret(first)}${this.#styleSuggestion(remaining)}`;
127
+ }
128
+ else {
129
+ content = this.#styleSuggestion(chunk.text);
130
+ }
131
+ const plainWidth = visibleWidth(chunk.text);
132
+ const padding = " ".repeat(Math.max(0, contentWidth - plainWidth));
133
+ const prefix = index === 0 ? this.#promptPrefix : " ".repeat(prefixWidth);
134
+ return truncateToWidth(`${prefix}${leftPadding}${content}${padding}${rightPadding}`, width);
135
+ });
136
+ return [horizontal, ...rows, horizontal];
97
137
  }
98
- for (const [action, handler] of this.actionHandlers) {
99
- if (action !== "app.interrupt" && action !== "app.exit" && this.keybindings.matches(data, action)) {
100
- handler();
101
- return;
102
- }
138
+ #renderPrefixedEditor(width) {
139
+ const prefixWidth = visibleWidth(this.#promptPrefix);
140
+ const innerWidth = Math.max(1, width - prefixWidth);
141
+ const rows = super.render(innerWidth);
142
+ const maxPadding = Math.max(0, Math.floor((innerWidth - 1) / 2));
143
+ const paddingX = Math.min(this.getPaddingX(), maxPadding);
144
+ const contentWidth = Math.max(1, innerWidth - paddingX * 2);
145
+ const layoutWidth = Math.max(1, contentWidth - (paddingX ? 0 : 1));
146
+ const layoutCount = this.getLines().flatMap(line => wrapTextWithAnsi(line, layoutWidth)).length || 1;
147
+ const visibleCount = Math.min(layoutCount, Math.max(5, Math.floor(this.#terminalRows() * 0.3)));
148
+ const bottomBorder = visibleCount + 1;
149
+ return rows.map((row, index) => {
150
+ if (index === 0 || index === bottomBorder)
151
+ return `${row}${this.borderColor("─".repeat(prefixWidth))}`;
152
+ return `${index === 1 ? this.#promptPrefix : " ".repeat(prefixWidth)}${row}`;
153
+ });
103
154
  }
104
- super.handleInput(data);
105
- }
106
- #renderSuggestion(width) {
107
- const prefixWidth = visibleWidth(this.#promptPrefix);
108
- const innerWidth = Math.max(1, width - prefixWidth);
109
- const maxPadding = Math.max(0, Math.floor((innerWidth - 1) / 2));
110
- const paddingX = Math.min(this.getPaddingX(), maxPadding);
111
- const contentWidth = Math.max(1, innerWidth - paddingX * 2);
112
- const layoutWidth = Math.max(1, contentWidth - (paddingX ? 0 : 1));
113
- const chunks = wrapTextWithAnsi(this.#promptSuggestion ?? "", layoutWidth).map(text => ({ text }));
114
- const maxVisible = Math.max(5, Math.floor(this.#terminalRows() * 0.3));
115
- const visible = chunks.slice(0, maxVisible);
116
- const horizontal = this.borderColor("─".repeat(Math.max(0, width)));
117
- const leftPadding = " ".repeat(paddingX);
118
- const rightPadding = leftPadding;
119
- const rows = visible.map((chunk, index) => {
120
- let content;
121
- if (index === 0) {
122
- const first = [...PROMPT_GRAPHEMES.segment(chunk.text)][0]?.segment ?? " ";
123
- const remaining = chunk.text.slice(first === " " && chunk.text.length === 0 ? 0 : first.length);
124
- const marker = this.focused ? CURSOR_MARKER : "";
125
- content = `${marker}${this.#styleSuggestionCaret(first)}${this.#styleSuggestion(remaining)}`;
126
- }
127
- else {
128
- content = this.#styleSuggestion(chunk.text);
129
- }
130
- const plainWidth = visibleWidth(chunk.text);
131
- const padding = " ".repeat(Math.max(0, contentWidth - plainWidth));
132
- const prefix = index === 0 ? this.#promptPrefix : " ".repeat(prefixWidth);
133
- return truncateToWidth(`${prefix}${leftPadding}${content}${padding}${rightPadding}`, width);
134
- });
135
- return [horizontal, ...rows, horizontal];
136
- }
137
- #renderPrefixedEditor(width) {
138
- const prefixWidth = visibleWidth(this.#promptPrefix);
139
- const innerWidth = Math.max(1, width - prefixWidth);
140
- const rows = super.render(innerWidth);
141
- const maxPadding = Math.max(0, Math.floor((innerWidth - 1) / 2));
142
- const paddingX = Math.min(this.getPaddingX(), maxPadding);
143
- const contentWidth = Math.max(1, innerWidth - paddingX * 2);
144
- const layoutWidth = Math.max(1, contentWidth - (paddingX ? 0 : 1));
145
- const layoutCount = this.getLines().flatMap(line => wrapTextWithAnsi(line, layoutWidth)).length || 1;
146
- const visibleCount = Math.min(layoutCount, Math.max(5, Math.floor(this.#terminalRows() * 0.3)));
147
- const bottomBorder = visibleCount + 1;
148
- return rows.map((row, index) => {
149
- if (index === 0 || index === bottomBorder)
150
- return `${row}${this.borderColor("─".repeat(prefixWidth))}`;
151
- return `${index === 1 ? this.#promptPrefix : " ".repeat(prefixWidth)}${row}`;
152
- });
153
- }
155
+ };
156
+ }
157
+ export class OwnedEditor extends createOwnedEditorClass(Editor) {
154
158
  }