@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.
- package/README.md +9 -0
- package/dist/composition/owned-ui.js +12 -1
- package/dist/contracts/owned-ui/index.d.ts +1 -0
- package/dist/contracts/owned-ui/index.js +1 -0
- package/dist/contracts/owned-ui/prompt-history.d.ts +37 -0
- package/dist/contracts/owned-ui/prompt-history.js +23 -0
- package/dist/features/prompt-history/index.d.ts +2 -0
- package/dist/features/prompt-history/index.js +2 -0
- package/dist/features/prompt-history/paths.d.ts +4 -0
- package/dist/features/prompt-history/paths.js +16 -0
- package/dist/features/prompt-history/service.d.ts +24 -0
- package/dist/features/prompt-history/service.js +260 -0
- package/dist/features/prompt-history/store.d.ts +15 -0
- package/dist/features/prompt-history/store.js +166 -0
- package/dist/features/prompt-history/worker.d.ts +9 -0
- package/dist/features/prompt-history/worker.js +42 -0
- package/dist/integrations/pi/components/editor-interaction.d.ts +38 -0
- package/dist/integrations/pi/components/editor-interaction.js +1 -0
- package/dist/integrations/pi/components/history-editor-loader.d.ts +5 -0
- package/dist/integrations/pi/components/history-editor-loader.js +5 -0
- package/dist/integrations/pi/components/index.d.ts +1 -0
- package/dist/integrations/pi/components/index.js +1 -0
- package/dist/integrations/pi/components/owned-editor-ux.d.ts +1 -1
- package/dist/integrations/pi/components/owned-editor-ux.js +41 -19
- package/dist/integrations/pi/components/shell-editor-autocomplete.js +9 -1
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +6 -0
- package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +13 -9
- package/dist/integrations/pi/components/upstream/components/owned-editor.js +137 -133
- package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +280 -0
- package/dist/integrations/pi/components/upstream/history/editor-core.js +2062 -0
- package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +32 -0
- package/dist/integrations/pi/components/upstream/history/kill-ring.js +48 -0
- package/dist/integrations/pi/components/upstream/history/printable-key.d.ts +1 -0
- package/dist/integrations/pi/components/upstream/history/printable-key.js +40 -0
- package/dist/integrations/pi/components/upstream/history/text-helpers.d.ts +14 -0
- package/dist/integrations/pi/components/upstream/history/text-helpers.js +28 -0
- package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +22 -0
- package/dist/integrations/pi/components/upstream/history/undo-stack.js +36 -0
- package/dist/integrations/pi/components/upstream/history/word-navigation.d.ts +24 -0
- package/dist/integrations/pi/components/upstream/history/word-navigation.js +100 -0
- package/dist/integrations/pi/session-ui/prompt-chips.d.ts +1 -0
- package/dist/integrations/pi/session-ui/prompt-chips.js +3 -0
- package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +30 -0
- package/dist/integrations/pi/session-ui/prompt-history-controller.js +147 -0
- package/dist/integrations/pi/session-ui/session-shell-root.d.ts +8 -0
- package/dist/integrations/pi/session-ui/session-shell-root.js +3 -0
- package/dist/integrations/pi/session-ui/session-shell.js +40 -5
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/product-identity.d.ts +1 -1
- package/dist/product-identity.js +1 -1
- package/dist/product-identity.json +1 -0
- package/dist/ui/settings/declarations.js +18 -0
- package/dist/ui/settings/session.js +13 -4
- package/docs/architecture/boundaries.md +1 -0
- package/docs/architecture/history-editor-provenance.md +96 -0
- package/docs/architecture/project-structure.md +1 -0
- package/docs/architecture/resource-and-data-policy.md +22 -1
- package/docs/features/prompt-history.md +121 -0
- 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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
678
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
16
|
-
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
41
|
+
setText(text) {
|
|
42
|
+
if (text.length > 0)
|
|
43
|
+
this.#promptSuggestion = null;
|
|
44
|
+
super.setText(text);
|
|
76
45
|
}
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
}
|