@timurproko/a1 0.1.8-dev.290 → 0.1.8-dev.322

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 (112) hide show
  1. package/README.md +21 -0
  2. package/bin/guardian.js +4 -3
  3. package/bin/ui.js +4 -2
  4. package/bin/update-recovery.js +3 -1
  5. package/dist/composition/owned-ui.js +13 -1
  6. package/dist/contracts/owned-ui/index.d.ts +1 -0
  7. package/dist/contracts/owned-ui/index.js +1 -0
  8. package/dist/contracts/owned-ui/prompt-history.d.ts +37 -0
  9. package/dist/contracts/owned-ui/prompt-history.js +23 -0
  10. package/dist/features/launch/profile-paths.d.ts +2 -0
  11. package/dist/features/launch/profile-paths.js +11 -0
  12. package/dist/features/prompt-history/index.d.ts +2 -0
  13. package/dist/features/prompt-history/index.js +2 -0
  14. package/dist/features/prompt-history/paths.d.ts +4 -0
  15. package/dist/features/prompt-history/paths.js +16 -0
  16. package/dist/features/prompt-history/service.d.ts +24 -0
  17. package/dist/features/prompt-history/service.js +260 -0
  18. package/dist/features/prompt-history/store.d.ts +15 -0
  19. package/dist/features/prompt-history/store.js +166 -0
  20. package/dist/features/prompt-history/worker.d.ts +9 -0
  21. package/dist/features/prompt-history/worker.js +42 -0
  22. package/dist/foundation/launch-context/index.d.ts +29 -0
  23. package/dist/foundation/launch-context/index.js +83 -0
  24. package/dist/foundation/launch-guardian/main.js +4 -3
  25. package/dist/foundation/release/bootstrap.d.ts +1 -1
  26. package/dist/foundation/release/bootstrap.js +18 -12
  27. package/dist/foundation/release/cohort-state.d.ts +1 -0
  28. package/dist/foundation/release/cohort-state.js +4 -0
  29. package/dist/foundation/release/release-gc.js +2 -2
  30. package/dist/foundation/release/release-store.js +10 -3
  31. package/dist/foundation/release/release.d.ts +3 -1
  32. package/dist/foundation/release/release.js +7 -3
  33. package/dist/foundation/release/restart-certification.d.ts +1 -0
  34. package/dist/foundation/release/restart-certification.js +5 -0
  35. package/dist/foundation/release/update-recovery.d.ts +2 -0
  36. package/dist/foundation/release/update-recovery.js +4 -0
  37. package/dist/foundation/release/update.js +5 -0
  38. package/dist/foundation/release/warmup.d.ts +1 -1
  39. package/dist/foundation/release/warmup.js +5 -4
  40. package/dist/foundation/startup/startup-runtime.js +13 -11
  41. package/dist/foundation/supervision/main.js +8 -7
  42. package/dist/integrations/pi/components/editor-interaction.d.ts +38 -0
  43. package/dist/integrations/pi/components/editor-interaction.js +1 -0
  44. package/dist/integrations/pi/components/history-editor-loader.d.ts +5 -0
  45. package/dist/integrations/pi/components/history-editor-loader.js +5 -0
  46. package/dist/integrations/pi/components/index.d.ts +1 -0
  47. package/dist/integrations/pi/components/index.js +1 -0
  48. package/dist/integrations/pi/components/owned-editor-ux.d.ts +2 -1
  49. package/dist/integrations/pi/components/owned-editor-ux.js +95 -22
  50. package/dist/integrations/pi/components/shell-editor-autocomplete.js +12 -1
  51. package/dist/integrations/pi/components/shell-presenters-info.d.ts +9 -2
  52. package/dist/integrations/pi/components/shell-presenters-info.js +35 -4
  53. package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +3 -2
  54. package/dist/integrations/pi/components/shell-selectors-dialogs.js +8 -5
  55. package/dist/integrations/pi/components/shell-shared-facade.d.ts +15 -2
  56. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +12 -0
  57. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +25 -2
  58. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +13 -9
  59. package/dist/integrations/pi/components/upstream/components/owned-editor.js +137 -133
  60. package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +280 -0
  61. package/dist/integrations/pi/components/upstream/history/editor-core.js +2062 -0
  62. package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +32 -0
  63. package/dist/integrations/pi/components/upstream/history/kill-ring.js +48 -0
  64. package/dist/integrations/pi/components/upstream/history/printable-key.d.ts +1 -0
  65. package/dist/integrations/pi/components/upstream/history/printable-key.js +40 -0
  66. package/dist/integrations/pi/components/upstream/history/text-helpers.d.ts +14 -0
  67. package/dist/integrations/pi/components/upstream/history/text-helpers.js +28 -0
  68. package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +22 -0
  69. package/dist/integrations/pi/components/upstream/history/undo-stack.js +36 -0
  70. package/dist/integrations/pi/components/upstream/history/word-navigation.d.ts +24 -0
  71. package/dist/integrations/pi/components/upstream/history/word-navigation.js +100 -0
  72. package/dist/integrations/pi/engine/adapter.d.ts +13 -1
  73. package/dist/integrations/pi/engine/adapter.js +86 -25
  74. package/dist/integrations/pi/engine/changelog.d.ts +2 -0
  75. package/dist/integrations/pi/engine/changelog.js +63 -0
  76. package/dist/integrations/pi/engine/resources/changelog.json +12 -0
  77. package/dist/integrations/pi/engine/workflow-controllers.d.ts +1 -1
  78. package/dist/integrations/pi/engine/workflows.d.ts +1 -1
  79. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +4 -1
  80. package/dist/integrations/pi/session-ui/prompt-chips.js +40 -13
  81. package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +30 -0
  82. package/dist/integrations/pi/session-ui/prompt-history-controller.js +147 -0
  83. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +33 -0
  84. package/dist/integrations/pi/session-ui/session-shell-root.js +68 -74
  85. package/dist/integrations/pi/session-ui/session-shell.d.ts +1 -1
  86. package/dist/integrations/pi/session-ui/session-shell.js +98 -12
  87. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +5 -0
  88. package/dist/integrations/pi/session-ui/session-viewport-controller.js +21 -15
  89. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +7 -0
  90. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +7 -6
  91. package/dist/native/darwin-arm64/manifest.json +1 -1
  92. package/dist/native/linux-x64/manifest.json +1 -1
  93. package/dist/native/win32-x64/manifest.json +2 -2
  94. package/dist/native/win32-x64/process-guardian.exe +0 -0
  95. package/dist/product-identity.d.ts +2 -2
  96. package/dist/product-identity.js +3 -36
  97. package/dist/product-identity.json +1 -22
  98. package/dist/ui/components/transcript-viewport.d.ts +8 -0
  99. package/dist/ui/components/transcript-viewport.js +23 -2
  100. package/dist/ui/components/visible-hyperlinks.d.ts +7 -0
  101. package/dist/ui/components/visible-hyperlinks.js +1 -0
  102. package/dist/ui/settings/declarations.js +19 -0
  103. package/dist/ui/settings/sections.js +11 -1
  104. package/dist/ui/settings/session.js +13 -4
  105. package/docs/architecture/boundaries.md +2 -0
  106. package/docs/architecture/history-editor-provenance.md +96 -0
  107. package/docs/architecture/internal-naming.md +89 -0
  108. package/docs/architecture/project-structure.md +1 -0
  109. package/docs/architecture/resource-and-data-policy.md +27 -1
  110. package/docs/architecture/toolchain.md +15 -1
  111. package/docs/features/prompt-history.md +149 -0
  112. package/package.json +8 -3
@@ -1,10 +1,28 @@
1
1
  // Mechanically adapted from Pi commit 914cf14
2
2
  // packages/coding-agent/src/modes/interactive/components/earendil-announcement.ts (MIT).
3
- // Local modifications: remap private imports to public package-root/owned theme boundaries and omit optional package-private imagery.
4
- import { Container, Spacer, Text } from "#pi-tui";
3
+ // Local modifications: remap private imports to public package-root/owned theme boundaries; load the verified owned copy of the image lazily.
4
+ import { readFileSync } from "node:fs";
5
+ import { Container, Image, Spacer, Text } from "#pi-tui";
5
6
  import { DynamicBorder } from "@earendil-works/pi-coding-agent";
6
7
  import { piTheme } from "../theme/theme.js";
7
8
  const BLOG_URL = "https://mariozechner.at/posts/2026-04-08-ive-sold-out/";
9
+ const IMAGE_FILENAME = "clankolas.png";
10
+ let cachedImageBase64;
11
+ let attemptedImageLoad = false;
12
+ function loadImageBase64() {
13
+ if (attemptedImageLoad)
14
+ return cachedImageBase64;
15
+ attemptedImageLoad = true;
16
+ try {
17
+ const resource = JSON.parse(readFileSync(new URL("../assets/earendil-image.json", import.meta.url), "utf8"));
18
+ if (resource && typeof resource === "object" && "data" in resource && typeof resource.data === "string")
19
+ cachedImageBase64 = resource.data;
20
+ }
21
+ catch {
22
+ cachedImageBase64 = undefined;
23
+ }
24
+ return cachedImageBase64;
25
+ }
8
26
  export class EarendilAnnouncementComponent extends Container {
9
27
  constructor() {
10
28
  super();
@@ -14,6 +32,11 @@ export class EarendilAnnouncementComponent extends Container {
14
32
  this.addChild(new Text(piTheme().fg("muted", "Read the blog post:"), 1, 0));
15
33
  this.addChild(new Text(piTheme().fg("mdLink", BLOG_URL), 1, 0));
16
34
  this.addChild(new Spacer(1));
35
+ const imageBase64 = loadImageBase64();
36
+ if (imageBase64) {
37
+ this.addChild(new Image(imageBase64, "image/png", { fallbackColor: text => piTheme().fg("muted", text) }, { maxWidthCells: 56, filename: IMAGE_FILENAME }));
38
+ this.addChild(new Spacer(1));
39
+ }
17
40
  this.addChild(new DynamicBorder((text) => piTheme().fg("accent", text)));
18
41
  }
19
42
  }
@@ -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
  }
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Adapted from @earendil-works/pi-tui 0.84.2, packages/tui/src/components/editor.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 { type AutocompleteProvider, type Component, type Focusable, type TUI, type SelectListTheme } from "#pi-tui";
7
+ import type { EditorInteractionPort, EditorRecallPort } from "../../editor-interaction.js";
8
+ /**
9
+ * Represents a chunk of text for word-wrap layout.
10
+ * Tracks both the text content and its position in the original line.
11
+ */
12
+ export interface TextChunk {
13
+ text: string;
14
+ startIndex: number;
15
+ endIndex: number;
16
+ }
17
+ /**
18
+ * Split a line into word-wrapped chunks.
19
+ * Wraps at word boundaries when possible, falling back to character-level
20
+ * wrapping for words longer than the available width.
21
+ *
22
+ * @param line - The text line to wrap
23
+ * @param maxWidth - Maximum visible width per chunk
24
+ * @param preSegmented - Optional pre-segmented graphemes (e.g. with paste-marker awareness).
25
+ * When omitted the default Intl.Segmenter is used.
26
+ * @returns Array of chunks with text and position information
27
+ */
28
+ export declare function wordWrapLine(line: string, maxWidth: number, preSegmented?: Intl.SegmentData[]): TextChunk[];
29
+ export interface EditorTheme {
30
+ borderColor: (str: string) => string;
31
+ selectList: SelectListTheme;
32
+ }
33
+ export interface EditorOptions {
34
+ persistentHistory?: boolean;
35
+ styleHistoryLabel?: (text: string) => string;
36
+ paddingX?: number;
37
+ autocompleteMaxVisible?: number;
38
+ }
39
+ export declare class HistoryEditorCore implements Component, Focusable {
40
+ private segmentTransform;
41
+ readonly interaction: EditorInteractionPort;
42
+ private state;
43
+ /** Focusable interface - set by TUI when focus changes */
44
+ focused: boolean;
45
+ protected tui: TUI;
46
+ private theme;
47
+ private paddingX;
48
+ private lastWidth;
49
+ private scrollOffset;
50
+ borderColor: (str: string) => string;
51
+ private autocompleteProvider?;
52
+ private autocompleteTriggerCharacters;
53
+ private autocompleteTriggerPattern;
54
+ private autocompleteDebouncePattern;
55
+ private autocompleteList;
56
+ private autocompleteState;
57
+ private autocompletePrefix;
58
+ private autocompleteMaxVisible;
59
+ private autocompleteAbort;
60
+ private autocompleteDebounceTimer;
61
+ private autocompleteRequestTask;
62
+ private autocompleteStartToken;
63
+ private autocompleteRequestId;
64
+ private pastes;
65
+ private pasteCounter;
66
+ private pasteBuffer;
67
+ private isInPaste;
68
+ private history;
69
+ private historyIndex;
70
+ private historyDraft;
71
+ readonly recall: EditorRecallPort;
72
+ private persistentHistory;
73
+ private readonly styleHistoryLabel;
74
+ private historyPasteDraft;
75
+ private pendingHistory;
76
+ onHistoryChange?: (state: {
77
+ readonly index: number;
78
+ readonly total: number;
79
+ }) => void;
80
+ configurePersistentHistory(enabled: boolean): void;
81
+ replaceHistoryEntries(entries: readonly string[]): void;
82
+ getHistoryPosition(): {
83
+ readonly index: number;
84
+ readonly total: number;
85
+ };
86
+ resetHistoryBrowsing(): void;
87
+ private applyPendingHistory;
88
+ private notifyHistory;
89
+ private killRing;
90
+ private lastAction;
91
+ private jumpMode;
92
+ private preferredVisualCol;
93
+ private snappedFromCursorCol;
94
+ private undoStack;
95
+ onSubmit?: (text: string) => void;
96
+ onChange?: (text: string) => void;
97
+ disableSubmit: boolean;
98
+ constructor(tui: TUI, theme: EditorTheme, options?: EditorOptions);
99
+ /** Set of currently valid paste IDs, for marker-aware segmentation. */
100
+ private validPasteIds;
101
+ /** Segment text with paste-marker awareness, only merging markers with valid IDs. */
102
+ private segment;
103
+ getPaddingX(): number;
104
+ setPaddingX(padding: number): void;
105
+ getAutocompleteMaxVisible(): number;
106
+ setAutocompleteMaxVisible(maxVisible: number): void;
107
+ setAutocompleteProvider(provider: AutocompleteProvider): void;
108
+ /**
109
+ * Add a prompt to history for up/down arrow navigation.
110
+ * Called after successful submission.
111
+ */
112
+ addToHistory(text: string): void;
113
+ private isEditorEmpty;
114
+ private isOnFirstVisualLine;
115
+ private isOnLastVisualLine;
116
+ private navigateHistory;
117
+ private exitHistoryBrowsing;
118
+ /** Internal setText that doesn't reset history state - used by navigateHistory */
119
+ private setTextInternal;
120
+ invalidate(): void;
121
+ render(width: number): string[];
122
+ handleInput(data: string): void;
123
+ private layoutText;
124
+ getText(): string;
125
+ private expandPasteMarkers;
126
+ /**
127
+ * Get text with paste markers expanded to their actual content.
128
+ * Use this when you need the full content (e.g., for external editor).
129
+ */
130
+ getExpandedText(): string;
131
+ getLines(): string[];
132
+ getCursor(): {
133
+ line: number;
134
+ col: number;
135
+ };
136
+ setText(text: string): void;
137
+ /**
138
+ * Insert text at the current cursor position.
139
+ * Used for programmatic insertion (e.g., clipboard image markers).
140
+ * This is atomic for undo - single undo restores entire pre-insert state.
141
+ */
142
+ insertTextAtCursor(text: string): void;
143
+ /**
144
+ * Normalize text for editor storage:
145
+ * - Normalize line endings (\r\n and \r -> \n)
146
+ * - Expand tabs to 4 spaces
147
+ */
148
+ private normalizeText;
149
+ /**
150
+ * Internal text insertion at cursor. Handles single and multi-line text.
151
+ * Does not push undo snapshots or trigger autocomplete - caller is responsible.
152
+ * Normalizes line endings and calls onChange once at the end.
153
+ */
154
+ private insertTextAtCursorInternal;
155
+ private insertCharacter;
156
+ private handlePaste;
157
+ private addNewLine;
158
+ private shouldSubmitOnBackslashEnter;
159
+ private submitValue;
160
+ private handleBackspace;
161
+ /**
162
+ * Set cursor column and clear preferredVisualCol.
163
+ * Use this for all non-vertical cursor movements to reset sticky column behavior.
164
+ */
165
+ private setCursorCol;
166
+ /**
167
+ * Move cursor to a target visual line, applying sticky column logic.
168
+ * Shared by moveCursor() and pageScroll().
169
+ */
170
+ private moveToVisualLine;
171
+ /**
172
+ * Compute the target visual column for vertical cursor movement.
173
+ * Implements the sticky column decision table:
174
+ *
175
+ * | P | S | T | U | Scenario | Set Preferred | Move To |
176
+ * |---|---|---|---| ---------------------------------------------------- |---------------|-------------|
177
+ * | 0 | * | 0 | - | Start nav, target fits | null | current |
178
+ * | 0 | * | 1 | - | Start nav, target shorter | current | target end |
179
+ * | 1 | 0 | 0 | 0 | Clamped, target fits preferred | null | preferred |
180
+ * | 1 | 0 | 0 | 1 | Clamped, target longer but still can't fit preferred | keep | target end |
181
+ * | 1 | 0 | 1 | - | Clamped, target even shorter | keep | target end |
182
+ * | 1 | 1 | 0 | - | Rewrapped, target fits current | null | current |
183
+ * | 1 | 1 | 1 | - | Rewrapped, target shorter than current | current | target end |
184
+ *
185
+ * Where:
186
+ * - P = preferred col is set
187
+ * - S = cursor in middle of source line (not clamped to end)
188
+ * - T = target line shorter than current visual col
189
+ * - U = target line shorter than preferred col
190
+ */
191
+ private computeVerticalMoveColumn;
192
+ private moveToLineStart;
193
+ private moveToLineEnd;
194
+ private deleteToStartOfLine;
195
+ private deleteToEndOfLine;
196
+ private deleteWordBackwards;
197
+ private deleteWordForward;
198
+ private handleForwardDelete;
199
+ /**
200
+ * Build a mapping from visual lines to logical positions.
201
+ * Returns an array where each element represents a visual line with:
202
+ * - logicalLine: index into this.state.lines
203
+ * - startCol: starting column in the logical line
204
+ * - length: length of this visual line segment
205
+ */
206
+ private buildVisualLineMap;
207
+ /**
208
+ * Find the visual line index that contains the given logical position.
209
+ */
210
+ private findVisualLineAt;
211
+ /**
212
+ * Find the visual line index for the current cursor position.
213
+ */
214
+ private findCurrentVisualLine;
215
+ private moveCursor;
216
+ /**
217
+ * Scroll by a page (direction: -1 for up, 1 for down).
218
+ * Moves cursor by the page size while keeping it in bounds.
219
+ */
220
+ private pageScroll;
221
+ private moveWordBackwards;
222
+ /**
223
+ * Yank (paste) the most recent kill ring entry at cursor position.
224
+ */
225
+ private yank;
226
+ /**
227
+ * Cycle through kill ring (only works immediately after yank or yank-pop).
228
+ * Replaces the last yanked text with the previous entry in the ring.
229
+ */
230
+ private yankPop;
231
+ /**
232
+ * Insert text at cursor position (used by yank operations).
233
+ */
234
+ private insertYankedText;
235
+ /**
236
+ * Delete the previously yanked text (used by yank-pop).
237
+ * The yanked text is derived from killRing[end] since it hasn't been rotated yet.
238
+ */
239
+ private deleteYankedText;
240
+ private pushUndoSnapshot;
241
+ private undo;
242
+ /**
243
+ * Jump to the first occurrence of a character in the specified direction.
244
+ * Multi-line search. Case-sensitive. Skips the current cursor position.
245
+ */
246
+ private jumpToChar;
247
+ private moveWordForwards;
248
+ private isSlashMenuAllowed;
249
+ private isAtStartOfMessage;
250
+ private isInSlashCommandContext;
251
+ /**
252
+ * Find the best autocomplete item index for the given prefix.
253
+ * Returns -1 if no match is found.
254
+ *
255
+ * Match priority:
256
+ * 1. Exact match (prefix === item.value) -> always selected
257
+ * 2. Prefix match -> first item whose value starts with prefix
258
+ * 3. No match -> -1 (keep default highlight)
259
+ *
260
+ * Matching is case-sensitive and checks item.value only.
261
+ */
262
+ private getBestAutocompleteMatchIndex;
263
+ private createAutocompleteList;
264
+ private tryTriggerAutocomplete;
265
+ private handleTabCompletion;
266
+ private handleSlashCommandCompletion;
267
+ private forceFileAutocomplete;
268
+ private requestAutocomplete;
269
+ private startAutocompleteRequest;
270
+ private setAutocompleteTriggerCharacters;
271
+ private getAutocompleteDebounceMs;
272
+ private runAutocompleteRequest;
273
+ private isAutocompleteRequestCurrent;
274
+ private applyAutocompleteSuggestions;
275
+ private cancelAutocompleteRequest;
276
+ private clearAutocompleteUi;
277
+ private cancelAutocomplete;
278
+ isShowingAutocomplete(): boolean;
279
+ private updateAutocomplete;
280
+ }