@visual-z/xeno 0.1.4 → 0.2.1
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/CHANGELOG.md +25 -0
- package/README.md +143 -33
- package/docs/install.md +23 -6
- package/docs/providers.md +147 -0
- package/package.json +8 -2
- package/src/apps/cli.ts +45 -18
- package/src/apps/commands.ts +32 -4
- package/src/apps/completions.ts +9 -6
- package/src/apps/export-cli.ts +24 -0
- package/src/apps/extension-ui.ts +330 -0
- package/src/apps/input.ts +16 -1
- package/src/apps/model-catalog.ts +42 -0
- package/src/apps/project-trust.ts +49 -0
- package/src/apps/render.ts +1 -0
- package/src/apps/resource-browser.ts +75 -0
- package/src/apps/resources.ts +25 -0
- package/src/apps/session-browser.ts +113 -0
- package/src/apps/session-picker.ts +55 -0
- package/src/apps/shell.ts +1177 -378
- package/src/apps/startup-input.ts +94 -0
- package/src/apps/startup.ts +413 -295
- package/src/core/action-defaults.ts +306 -0
- package/src/core/attachment-input.ts +17 -0
- package/src/core/catalog-merge.ts +19 -0
- package/src/core/event-validation.ts +19 -2
- package/src/core/events.ts +45 -2
- package/src/core/export.ts +6 -2
- package/src/core/extension-flags.ts +14 -0
- package/src/core/images.ts +46 -0
- package/src/core/json-event.ts +2 -0
- package/src/core/keymap.ts +43 -119
- package/src/core/model-replay.ts +22 -0
- package/src/core/model-scope.ts +53 -0
- package/src/core/packages.ts +47 -0
- package/src/core/project.ts +21 -3
- package/src/core/prompts.ts +33 -5
- package/src/core/queue.ts +18 -0
- package/src/core/resource-selection.ts +36 -0
- package/src/{runtime → core}/resume.ts +15 -22
- package/src/core/session-archive.ts +4 -0
- package/src/core/session-browser.ts +82 -0
- package/src/core/session-entry.ts +56 -0
- package/src/core/session-position.ts +26 -0
- package/src/core/settings.ts +169 -154
- package/src/core/theme.ts +15 -6
- package/src/core/thinking.ts +26 -0
- package/src/core/tokens.ts +2 -0
- package/src/core/transcript.ts +15 -5
- package/src/runtime/auth.ts +116 -11
- package/src/runtime/catalog.ts +54 -151
- package/src/runtime/clipboard.ts +114 -0
- package/src/runtime/extension-loader.ts +37 -0
- package/src/runtime/extension-ui.ts +55 -0
- package/src/runtime/extensions.ts +273 -0
- package/src/runtime/images.ts +76 -0
- package/src/runtime/loop.ts +213 -83
- package/src/runtime/model-config.ts +232 -0
- package/src/runtime/model-data.LICENSE +27 -0
- package/src/runtime/model-data.ts +21 -0
- package/src/runtime/model-replay.ts +10 -0
- package/src/runtime/model-scope.ts +20 -0
- package/src/runtime/model-sources.ts +151 -0
- package/src/runtime/model.ts +74 -11
- package/src/runtime/models-fetch.ts +6 -104
- package/src/runtime/oauth.ts +4 -3
- package/src/runtime/openai-responses.ts +12 -4
- package/src/runtime/openai.ts +13 -4
- package/src/runtime/package-manager.ts +125 -0
- package/src/runtime/pi-provider.ts +235 -0
- package/src/runtime/progress.ts +26 -0
- package/src/runtime/provider-access.ts +21 -0
- package/src/runtime/provider-protocols.ts +29 -0
- package/src/runtime/provider-request.ts +73 -0
- package/src/runtime/provider.ts +26 -6
- package/src/runtime/rich-text.ts +36 -0
- package/src/runtime/tools.ts +41 -5
- package/src/store/command.ts +7 -1
- package/src/store/context-files.ts +1 -1
- package/src/store/log.ts +65 -0
- package/src/store/models.ts +28 -9
- package/src/store/observations.ts +4 -2
- package/src/store/packages.ts +98 -0
- package/src/store/recovery.ts +8 -0
- package/src/store/resource-index.ts +170 -0
- package/src/store/resources.ts +75 -142
- package/src/store/session-archive.ts +162 -0
- package/src/store/session-browser.ts +68 -0
- package/src/store/session-position.ts +27 -0
- package/src/store/sessions.ts +87 -66
- package/src/store/trust.ts +13 -0
- package/src/tui/chat-viewport.ts +47 -0
- package/src/tui/editor.ts +62 -15
- package/src/tui/host.ts +50 -9
- package/src/tui/input.ts +8 -2
- package/src/tui/select.ts +24 -6
- package/src/tui/settings.ts +4 -0
- package/src/tui/system-theme.ts +700 -0
- package/src/tui/transcript.ts +63 -11
- package/src/tui/tree.ts +161 -95
- package/src/vendor/pi-rich/LICENSE.upstream +21 -0
- package/src/vendor/pi-rich/UPSTREAM.json +17 -0
- package/src/vendor/pi-rich/clipboard-command.ts +44 -0
- package/src/vendor/pi-rich/exif-orientation.ts +182 -0
- package/src/vendor/pi-rich/html.ts +51 -0
- package/src/vendor/pi-rich/image-convert.ts +49 -0
- package/src/vendor/pi-rich/image-resize-core.ts +164 -0
- package/src/vendor/pi-rich/image-resize-worker.ts +42 -0
- package/src/vendor/pi-rich/image-resize.ts +123 -0
- package/src/vendor/pi-rich/mime.ts +116 -0
- package/src/vendor/pi-rich/photon.ts +139 -0
- package/src/vendor/pi-rich/syntax-highlight.ts +214 -0
- package/src/vendor/pi-tui/UPSTREAM.json +50 -0
- package/src/vendor/pi-tui/VENDOR.md +7 -5
- package/src/vendor/pi-tui/autocomplete.ts +35 -10
- package/src/vendor/pi-tui/colors.ts +367 -0
- package/src/vendor/pi-tui/components/box.ts +8 -9
- package/src/vendor/pi-tui/components/editor.ts +4 -2
- package/src/vendor/pi-tui/components/markdown.ts +8 -2
- package/src/vendor/pi-tui/index.ts +31 -1
- package/src/vendor/pi-tui/native/UPSTREAM.json +23 -0
- package/src/vendor/pi-tui/native/clipboard.h +208 -0
- package/src/vendor/pi-tui/native/darwin/README.md +22 -0
- package/src/vendor/pi-tui/native/darwin/build.sh +69 -0
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +132 -0
- package/src/vendor/pi-tui/native/linux/README.md +23 -0
- package/src/vendor/pi-tui/native/linux/build.sh +65 -0
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +70 -0
- package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +274 -0
- package/src/vendor/pi-tui/native/napi.h +91 -0
- package/src/vendor/pi-tui/native/win32/README.md +31 -0
- package/src/vendor/pi-tui/native/win32/build.mjs +232 -0
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/src/win32-platform.c +241 -0
- package/src/vendor/pi-tui/native-platform.ts +2 -0
- package/src/vendor/pi-tui/oklab.ts +233 -0
- package/src/vendor/pi-tui/terminal-colors.ts +26 -8
- package/src/vendor/pi-tui/terminal-image.ts +43 -8
- package/src/vendor/pi-tui/terminal.ts +16 -9
- package/src/vendor/pi-tui/tui-alt-screen.ts +26 -18
- package/src/vendor/pi-tui/tui.ts +112 -75
- package/src/vendor/pi-tui/utils.ts +89 -49
- package/src/vendor/pi-tui/wheel-scroll.ts +82 -0
package/src/tui/editor.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { Editor as PiEditor, type TUI } from "../vendor/pi-tui/index.ts";
|
|
20
20
|
import { getKeybindings, setKeybindings, type KeybindingsManager } from "../vendor/pi-tui/keybindings.ts";
|
|
21
21
|
import type { AutocompleteProvider } from "../vendor/pi-tui/autocomplete.ts";
|
|
22
|
+
import type { EditorComponent } from "../vendor/pi-tui/editor-component.ts";
|
|
22
23
|
import { encodeKey } from "./input.ts";
|
|
23
24
|
import { IDENTITY, PLAIN_ATTRS, paint, type Style } from "./style.ts";
|
|
24
25
|
import { completePath, pathTokenAtCursor, type PathToken } from "./complete.ts";
|
|
@@ -763,6 +764,10 @@ function maskSecret(text: string): string {
|
|
|
763
764
|
return out;
|
|
764
765
|
}
|
|
765
766
|
|
|
767
|
+
class OwnedPiEditor extends PiEditor {
|
|
768
|
+
rebind(tui: TUI): void { this.tui = tui; }
|
|
769
|
+
}
|
|
770
|
+
|
|
766
771
|
export class Editor implements Component {
|
|
767
772
|
private secret: boolean;
|
|
768
773
|
private submitted: string | null = null;
|
|
@@ -778,8 +783,13 @@ export class Editor implements Component {
|
|
|
778
783
|
* turn will spend (pi's behaviour). Identity until the Tui sets it.
|
|
779
784
|
*/
|
|
780
785
|
borderColor: (s: string) => string = (s) => s;
|
|
781
|
-
|
|
782
|
-
private pi:
|
|
786
|
+
host: TUI;
|
|
787
|
+
private pi: OwnedPiEditor;
|
|
788
|
+
private custom: (EditorComponent & { dispose?(): void; focused?: boolean }) | undefined;
|
|
789
|
+
private lastCustomText = "";
|
|
790
|
+
onComponentError: ((error: unknown) => void) | undefined;
|
|
791
|
+
private padding = 0;
|
|
792
|
+
private autocompleteRows = 5;
|
|
783
793
|
|
|
784
794
|
constructor(opts: CreateEditorOptions = {}) {
|
|
785
795
|
this.secret = opts.secret ?? false;
|
|
@@ -790,9 +800,9 @@ export class Editor implements Component {
|
|
|
790
800
|
this.pi.setText(opts.text ?? "");
|
|
791
801
|
}
|
|
792
802
|
|
|
793
|
-
private createPi():
|
|
803
|
+
private createPi(): OwnedPiEditor {
|
|
794
804
|
const theme = identityEditorTheme();
|
|
795
|
-
const pi = new
|
|
805
|
+
const pi = new OwnedPiEditor(this.host, {
|
|
796
806
|
...theme,
|
|
797
807
|
borderColor: (s) => this.borderColor(s),
|
|
798
808
|
selectList: {
|
|
@@ -803,6 +813,8 @@ export class Editor implements Component {
|
|
|
803
813
|
noMatch: (s) => paint(s, { ...PLAIN_ATTRS, dim: true }, this.style),
|
|
804
814
|
},
|
|
805
815
|
});
|
|
816
|
+
pi.setPaddingX(this.padding);
|
|
817
|
+
pi.setAutocompleteMaxVisible(this.autocompleteRows);
|
|
806
818
|
if (!this.secret) {
|
|
807
819
|
for (const text of [...this.history].reverse()) pi.addToHistory(text);
|
|
808
820
|
if (this.provider) pi.setAutocompleteProvider(this.provider);
|
|
@@ -818,8 +830,40 @@ export class Editor implements Component {
|
|
|
818
830
|
return pi;
|
|
819
831
|
}
|
|
820
832
|
|
|
821
|
-
|
|
822
|
-
|
|
833
|
+
/** Application key ownership stays outside the replaceable text component. */
|
|
834
|
+
setComponent(component: (EditorComponent & { dispose?(): void; focused?: boolean }) | undefined): void {
|
|
835
|
+
const text = this.state.text, focused = this.focused;
|
|
836
|
+
try { this.custom?.dispose?.(); } catch (error) { this.onComponentError?.(error); }
|
|
837
|
+
this.custom = component;
|
|
838
|
+
if (component) {
|
|
839
|
+
component.onSubmit = text => {
|
|
840
|
+
this.submitted = text;
|
|
841
|
+
if (text.trim()) { component.addToHistory?.(text); if (this.history[0] !== text) this.history.unshift(text); this.history = this.history.slice(0, this.historyLimit); }
|
|
842
|
+
};
|
|
843
|
+
component.setPaddingX?.(this.padding); component.setAutocompleteMaxVisible?.(this.autocompleteRows);
|
|
844
|
+
if (this.provider) component.setAutocompleteProvider?.(this.provider);
|
|
845
|
+
for (const entry of [...this.history].reverse()) component.addToHistory?.(entry);
|
|
846
|
+
}
|
|
847
|
+
this.applyExternal(text); this.focused = focused;
|
|
848
|
+
}
|
|
849
|
+
private get active(): EditorComponent & { focused?: boolean } { return !this.secret && this.custom ? this.custom : this.pi; }
|
|
850
|
+
private using<T>(operation: (editor: EditorComponent & { focused?: boolean }) => T): T {
|
|
851
|
+
const active = this.active;
|
|
852
|
+
try { return operation(active); } catch (error) {
|
|
853
|
+
if (active === this.pi) throw error;
|
|
854
|
+
const broken = this.custom; this.custom = undefined; this.pi.setText(this.lastCustomText); this.pi.focused = true;
|
|
855
|
+
try { broken?.dispose?.(); } catch { /* Report the original component failure. */ }
|
|
856
|
+
this.onComponentError?.(error); return operation(this.pi);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
get focused(): boolean { return this.using(editor => editor.focused ?? false); }
|
|
860
|
+
rebind(tui: TUI): void { this.host = tui; this.pi.rebind(tui); }
|
|
861
|
+
configure(padding: number, rows: number): void {
|
|
862
|
+
this.padding = padding; this.autocompleteRows = rows;
|
|
863
|
+
this.pi.setPaddingX(padding); this.pi.setAutocompleteMaxVisible(rows);
|
|
864
|
+
this.custom?.setPaddingX?.(padding); this.custom?.setAutocompleteMaxVisible?.(rows);
|
|
865
|
+
}
|
|
866
|
+
set focused(value: boolean) { this.using(editor => { editor.focused = value; }); }
|
|
823
867
|
setStyle(style: Style): void { this.style = style; }
|
|
824
868
|
|
|
825
869
|
handleInput(data: string): void {
|
|
@@ -831,26 +875,27 @@ export class Editor implements Component {
|
|
|
831
875
|
this.submitted = null;
|
|
832
876
|
const previous = getKeybindings();
|
|
833
877
|
if (this.bindings) setKeybindings(this.bindings);
|
|
834
|
-
try { this.
|
|
878
|
+
try { this.using(editor => editor.handleInput(data)); }
|
|
835
879
|
finally { setKeybindings(previous); }
|
|
836
880
|
return this.state;
|
|
837
881
|
}
|
|
838
882
|
|
|
839
|
-
get autocompleteVisible(): boolean { return this.pi.isShowingAutocomplete(); }
|
|
883
|
+
get autocompleteVisible(): boolean { return this.active === this.pi && this.pi.isShowingAutocomplete(); }
|
|
840
884
|
|
|
841
885
|
setAutocompleteProvider(provider: AutocompleteProvider): void {
|
|
842
886
|
this.provider = provider;
|
|
843
|
-
if (!this.secret) this.
|
|
887
|
+
if (!this.secret) this.using(editor => editor.setAutocompleteProvider?.(provider));
|
|
844
888
|
}
|
|
845
889
|
|
|
846
890
|
invalidate(): void {
|
|
847
|
-
this.
|
|
891
|
+
this.using(editor => editor.invalidate());
|
|
848
892
|
}
|
|
849
893
|
|
|
850
894
|
get state(): EditorState {
|
|
851
|
-
const text = this.
|
|
895
|
+
const text = this.using(editor => editor.getExpandedText?.() ?? editor.getText());
|
|
896
|
+
if (!this.secret) this.lastCustomText = text;
|
|
852
897
|
const pos = this.pi.getCursor();
|
|
853
|
-
const cursor = this.pi.getLines().slice(0, pos.line).reduce((n, line) => n + line.length + 1, 0) + pos.col;
|
|
898
|
+
const cursor = this.active === this.pi ? this.pi.getLines().slice(0, pos.line).reduce((n, line) => n + line.length + 1, 0) + pos.col : text.length;
|
|
854
899
|
return { ...createEditor(), text, cursor, submitted: this.submitted, secret: this.secret,
|
|
855
900
|
history: this.history, historyLimit: this.historyLimit };
|
|
856
901
|
}
|
|
@@ -860,7 +905,8 @@ export class Editor implements Component {
|
|
|
860
905
|
}
|
|
861
906
|
|
|
862
907
|
applyExternal(text: string): EditorState {
|
|
863
|
-
this.
|
|
908
|
+
if (!this.secret) this.lastCustomText = text;
|
|
909
|
+
this.using(editor => editor.setText(text));
|
|
864
910
|
this.submitted = null;
|
|
865
911
|
return this.state;
|
|
866
912
|
}
|
|
@@ -868,12 +914,13 @@ export class Editor implements Component {
|
|
|
868
914
|
setSecret(secret: boolean): EditorState {
|
|
869
915
|
if (this.secret === secret) return this.state;
|
|
870
916
|
const focused = this.focused;
|
|
871
|
-
const text = this.secret ? "" : this.
|
|
917
|
+
const text = this.secret ? "" : this.state.text;
|
|
872
918
|
this.secret = secret;
|
|
873
919
|
// Discard secret undo/history at the authentication boundary.
|
|
874
920
|
this.pi = this.createPi();
|
|
875
921
|
this.pi.focused = focused;
|
|
876
922
|
this.pi.setText(text);
|
|
923
|
+
if (!secret) this.using(editor => editor.setText(text));
|
|
877
924
|
return this.state;
|
|
878
925
|
}
|
|
879
926
|
|
|
@@ -888,6 +935,6 @@ export class Editor implements Component {
|
|
|
888
935
|
for (const _ of this.pi.getText().slice(this.state.cursor)) masked.handleInput("\x1b[D");
|
|
889
936
|
return masked.render(width);
|
|
890
937
|
}
|
|
891
|
-
return this.
|
|
938
|
+
return this.using(editor => { editor.borderColor = this.borderColor; return editor.render(width); });
|
|
892
939
|
}
|
|
893
940
|
}
|
package/src/tui/host.ts
CHANGED
|
@@ -21,11 +21,28 @@ import {
|
|
|
21
21
|
type Terminal,
|
|
22
22
|
type TUI,
|
|
23
23
|
TuiMainScreen,
|
|
24
|
+
TuiAltScreen,
|
|
24
25
|
stripTerminalSequences,
|
|
25
26
|
} from "../vendor/pi-tui/index.ts";
|
|
26
27
|
|
|
27
28
|
export { Container, Markdown, PiText };
|
|
28
29
|
export type { TuiMainScreen };
|
|
30
|
+
/** A single document inset keeps committed and streaming messages aligned. */
|
|
31
|
+
export class PaddedDocument extends Container {
|
|
32
|
+
padding = 1;
|
|
33
|
+
override render(width: number): string[] {
|
|
34
|
+
const padding = Math.min(this.padding, Math.max(0, Math.floor((width - 1) / 2)));
|
|
35
|
+
return super.render(Math.max(1, width - padding * 2)).map(line => line.startsWith("\x1b]133;A\x07")
|
|
36
|
+
? "\x1b]133;A\x07" + " ".repeat(padding) + line.slice(8) : " ".repeat(padding) + line);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export type InteractiveScreen = TuiMainScreen | TuiAltScreen;
|
|
40
|
+
export interface ScreenOptions {
|
|
41
|
+
tuiMode?: "regular" | "fullscreen";
|
|
42
|
+
fullscreenCopyOnSelect?: boolean;
|
|
43
|
+
fullscreenWheelScrollLines?: "auto" | number;
|
|
44
|
+
copySelection?: (text: string) => Promise<boolean | string>;
|
|
45
|
+
}
|
|
29
46
|
|
|
30
47
|
/**
|
|
31
48
|
* Wraps the injectible {columns, rows, write} terminal tests already use.
|
|
@@ -76,8 +93,19 @@ export class AdapterTerminal implements Terminal {
|
|
|
76
93
|
}
|
|
77
94
|
|
|
78
95
|
/** TuiMainScreen without ProcessTerminal — tests inject a FakeTerminal. */
|
|
79
|
-
export function createInteractiveTui(term?: SimpleTerminal, showHardwareCursor = true):
|
|
80
|
-
return
|
|
96
|
+
export function createInteractiveTui(term?: SimpleTerminal, showHardwareCursor = true, options: ScreenOptions = {}): InteractiveScreen {
|
|
97
|
+
return screenFor(term ? new AdapterTerminal(term) : new ProcessTerminal(), showHardwareCursor, options);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function replaceInteractiveTui(previous: InteractiveScreen, options: ScreenOptions): InteractiveScreen {
|
|
101
|
+
return screenFor(previous.terminal, previous.getShowHardwareCursor(), options);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function screenFor(terminal: Terminal, cursor: boolean, options: ScreenOptions): InteractiveScreen {
|
|
105
|
+
return options.tuiMode === "fullscreen" ? new TuiAltScreen(terminal, cursor, undefined, {
|
|
106
|
+
wheelScrollLines: options.fullscreenWheelScrollLines ?? "auto", copyOnSelect: options.fullscreenCopyOnSelect ?? true,
|
|
107
|
+
...(options.copySelection ? { copySelection: options.copySelection } : {}), scrollToEndIndicator: () => "↓ Latest",
|
|
108
|
+
}) : new TuiMainScreen(terminal, cursor);
|
|
81
109
|
}
|
|
82
110
|
|
|
83
111
|
/**
|
|
@@ -145,8 +173,13 @@ export function renderMarkdownLines(text: string, width: number, style: Style):
|
|
|
145
173
|
}
|
|
146
174
|
|
|
147
175
|
/** Activity-area assistant text: pi's Markdown, themed from our Style. */
|
|
148
|
-
export function assistantMarkdown(text: string, style: Style): Component {
|
|
149
|
-
return new MarkdownBlock(text, style);
|
|
176
|
+
export function assistantMarkdown(text: string, style: Style, features?: MarkdownFeatures): Component {
|
|
177
|
+
return new MarkdownBlock(text, style, features);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface MarkdownFeatures {
|
|
181
|
+
highlight: (code: string, language: string | undefined, style: Style) => string[];
|
|
182
|
+
transform: (text: string, width: number, streaming: boolean, style: Style, messageType?: "user" | "assistant" | "assistant-thinking") => string;
|
|
150
183
|
}
|
|
151
184
|
|
|
152
185
|
/** Keep Pi's parse/layout cache alive across keystrokes and streaming deltas. */
|
|
@@ -154,19 +187,27 @@ export class MarkdownBlock implements Component {
|
|
|
154
187
|
private readonly markdown: Markdown;
|
|
155
188
|
private readonly style: Style;
|
|
156
189
|
private text: string;
|
|
157
|
-
|
|
190
|
+
private readonly features: MarkdownFeatures | undefined;
|
|
191
|
+
private readonly streaming: boolean;
|
|
192
|
+
private renderedText: string | undefined;
|
|
193
|
+
constructor(text: string, style: Style, features?: MarkdownFeatures, streaming = false) {
|
|
158
194
|
this.text = text;
|
|
159
195
|
this.style = style;
|
|
160
|
-
this.
|
|
196
|
+
this.features = features; this.streaming = streaming;
|
|
197
|
+
this.markdown = new Markdown(text, 0, 0, { ...markdownThemeFromStyle(style),
|
|
198
|
+
...(features && style !== IDENTITY ? { highlightCode: (code: string, lang?: string) => features.highlight(code, lang, style) } : {}),
|
|
199
|
+
}, defaultTextStyleFrom(style));
|
|
161
200
|
}
|
|
162
201
|
setText(text: string): void {
|
|
163
202
|
if (text === this.text) return;
|
|
164
203
|
this.text = text;
|
|
165
|
-
this.
|
|
204
|
+
this.renderedText = undefined;
|
|
166
205
|
}
|
|
167
206
|
invalidate(): void { this.markdown.invalidate(); }
|
|
168
207
|
render(width: number): string[] {
|
|
169
208
|
if (width <= 0) return [""];
|
|
209
|
+
const transformed = this.features?.transform(this.text, width, this.streaming, this.style) ?? this.text;
|
|
210
|
+
if (this.renderedText !== transformed) { this.markdown.setText(transformed); this.renderedText = transformed; }
|
|
170
211
|
const lines = this.markdown.render(Math.max(1, width)).map((line) => line.trimEnd());
|
|
171
212
|
return this.style === IDENTITY ? lines.map(stripTerminalSequences) : lines;
|
|
172
213
|
}
|
|
@@ -205,8 +246,8 @@ export function markdownThemeFromStyle(style: Style): MarkdownTheme {
|
|
|
205
246
|
listBullet: wrapAttr(style, { ...PLAIN_ATTRS, dim: true }),
|
|
206
247
|
bold: wrapAttr(style, { ...PLAIN_ATTRS, bold: true }),
|
|
207
248
|
italic: wrapAttr(style, { ...PLAIN_ATTRS, italic: true }),
|
|
208
|
-
strikethrough: (t) => t
|
|
209
|
-
underline: (t) => t
|
|
249
|
+
strikethrough: (t) => `\x1b[9m${t}\x1b[29m`,
|
|
250
|
+
underline: (t) => `\x1b[4m${t}\x1b[24m`,
|
|
210
251
|
};
|
|
211
252
|
}
|
|
212
253
|
|
package/src/tui/input.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Pi owns decoding. Encoding exists only for programmatic actions and legacy tests. */
|
|
2
|
-
import { KeybindingsManager, TUI_KEYBINDINGS, type KeybindingsConfig } from "../vendor/pi-tui/keybindings.ts";
|
|
2
|
+
import { KeybindingsManager, TUI_KEYBINDINGS, setKeybindings, type KeybindingsConfig } from "../vendor/pi-tui/keybindings.ts";
|
|
3
3
|
import { matchesKey, parseKey, type KeyId } from "../vendor/pi-tui/keys.ts";
|
|
4
4
|
import { formatTuiKey, type Key } from "./keys.ts";
|
|
5
5
|
|
|
@@ -62,8 +62,14 @@ const INPUT_ACTIONS: Record<string, string> = {
|
|
|
62
62
|
export function editorBindings(byAction: Readonly<Record<string, readonly string[]>>): KeybindingsManager {
|
|
63
63
|
const config: KeybindingsConfig = {};
|
|
64
64
|
for (const [action, chords] of Object.entries(byAction)) {
|
|
65
|
-
const id = INPUT_ACTIONS[action] ?? `tui.${action}`;
|
|
65
|
+
const id = action.startsWith("tui.") ? action : INPUT_ACTIONS[action] ?? `tui.${action}`;
|
|
66
66
|
if (id in TUI_KEYBINDINGS) config[id] = [...chords] as KeyId[];
|
|
67
67
|
}
|
|
68
68
|
return new KeybindingsManager(TUI_KEYBINDINGS, config);
|
|
69
69
|
}
|
|
70
|
+
|
|
71
|
+
export function installBindings(byAction: Readonly<Record<string, readonly string[]>>): KeybindingsManager {
|
|
72
|
+
const manager = editorBindings(byAction);
|
|
73
|
+
setKeybindings(manager);
|
|
74
|
+
return manager;
|
|
75
|
+
}
|
package/src/tui/select.ts
CHANGED
|
@@ -15,7 +15,8 @@ import { Input } from "../vendor/pi-tui/components/input.ts";
|
|
|
15
15
|
import { SelectList as PiSelectList } from "../vendor/pi-tui/components/select-list.ts";
|
|
16
16
|
import { fuzzyFilter as piFuzzyFilter } from "../vendor/pi-tui/fuzzy.ts";
|
|
17
17
|
import { truncateToWidth } from "../vendor/pi-tui/utils.ts";
|
|
18
|
-
import {
|
|
18
|
+
import { getKeybindings } from "../vendor/pi-tui/keybindings.ts";
|
|
19
|
+
import { encodeKey } from "./input.ts";
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Default visible rows. Assumed, not measured: enough to scan a /model
|
|
@@ -37,6 +38,7 @@ export interface CreateSelectOptions {
|
|
|
37
38
|
readonly selectedValue?: string;
|
|
38
39
|
readonly search?: boolean;
|
|
39
40
|
readonly hint?: string;
|
|
41
|
+
readonly filter?: (items: readonly SelectItem[], query: string) => readonly SelectItem[];
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
export interface SelectState {
|
|
@@ -167,7 +169,7 @@ export function renderSelect(state: SelectState, width: number, style?: Style):
|
|
|
167
169
|
export class SelectList implements Component {
|
|
168
170
|
private current: SelectState;
|
|
169
171
|
private style: Style | undefined;
|
|
170
|
-
private
|
|
172
|
+
private items: readonly SelectItem[];
|
|
171
173
|
private readonly options: CreateSelectOptions;
|
|
172
174
|
private readonly search = new Input({ prompt: "Search: " });
|
|
173
175
|
private list: PiSelectList;
|
|
@@ -192,6 +194,7 @@ export class SelectList implements Component {
|
|
|
192
194
|
}
|
|
193
195
|
|
|
194
196
|
get focused(): boolean { return this.search.focused; }
|
|
197
|
+
get query(): string { return this.search.getValue(); }
|
|
195
198
|
set focused(value: boolean) { this.search.focused = value; }
|
|
196
199
|
invalidate(): void { this.search.invalidate(); this.list.invalidate(); }
|
|
197
200
|
handleInput(data: string): void { if (this.onInput) this.onInput(data); else this.acceptInput(data); }
|
|
@@ -204,14 +207,26 @@ export class SelectList implements Component {
|
|
|
204
207
|
this.list.setSelectedIndex(selected);
|
|
205
208
|
}
|
|
206
209
|
|
|
210
|
+
setItems(items: readonly SelectItem[]): void {
|
|
211
|
+
const selected = this.state.items[this.state.selected]?.value;
|
|
212
|
+
this.items = items;
|
|
213
|
+
const filtered = this.filter(items, this.search.getValue());
|
|
214
|
+
this.current = { ...this.current, items: filtered, selected: Math.max(0, filtered.findIndex(item => item.value === selected)) };
|
|
215
|
+
this.list = this.makeList();
|
|
216
|
+
this.list.setSelectedIndex(this.current.selected);
|
|
217
|
+
}
|
|
218
|
+
|
|
207
219
|
acceptInput(data: string): SelectState {
|
|
208
220
|
this.current = { ...this.current, confirmed: null, cancelled: false };
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
221
|
+
const kb = getKeybindings();
|
|
222
|
+
if ((["tui.select.up", "tui.select.down", "tui.select.confirm", "tui.select.cancel"] as const).some(key => kb.matches(data, key))) this.list.handleInput(data);
|
|
223
|
+
else if (kb.matches(data, "tui.select.pageUp") || kb.matches(data, "tui.select.pageDown")) {
|
|
224
|
+
this.list.setSelectedIndex(this.state.selected + (kb.matches(data, "tui.select.pageUp") ? -1 : 1) * this.current.maxVisible);
|
|
212
225
|
} else if (this.options.search !== false) {
|
|
226
|
+
const before = this.search.getValue();
|
|
213
227
|
this.search.handleInput(data);
|
|
214
|
-
|
|
228
|
+
if (before === this.search.getValue()) return this.state;
|
|
229
|
+
const filtered = this.filter(this.items, this.search.getValue());
|
|
215
230
|
this.current = { ...this.current, items: filtered, selected: 0 };
|
|
216
231
|
this.list = this.makeList();
|
|
217
232
|
}
|
|
@@ -228,6 +243,9 @@ export class SelectList implements Component {
|
|
|
228
243
|
list.onCancel = () => { this.current = { ...this.current, cancelled: true }; };
|
|
229
244
|
return list;
|
|
230
245
|
}
|
|
246
|
+
private filter(items: readonly SelectItem[], query: string): readonly SelectItem[] {
|
|
247
|
+
return this.options.filter?.(items, query) ?? piFuzzyFilter([...items], query, item => `${item.label} ${item.description ?? ""}`);
|
|
248
|
+
}
|
|
231
249
|
|
|
232
250
|
render(width: number): string[] {
|
|
233
251
|
if (width <= 0) return [""];
|
package/src/tui/settings.ts
CHANGED
|
@@ -31,6 +31,10 @@ export class Settings {
|
|
|
31
31
|
return this.changes;
|
|
32
32
|
}
|
|
33
33
|
updateValue(id: string, value: string): void { this.list.updateValue(id, value); }
|
|
34
|
+
updateValues(id: string, values: string[]): void {
|
|
35
|
+
const item = this.items.find(item => item.id === id);
|
|
36
|
+
if (item) item.values = values;
|
|
37
|
+
}
|
|
34
38
|
invalidate(): void { this.list.invalidate(); }
|
|
35
39
|
render(width: number): string[] {
|
|
36
40
|
if (width <= 0) return [""];
|