@linxiraos/pi-tui 1.2.1 → 1.3.0
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 +188 -125
- package/README.md +36 -0
- package/THIRD-PARTY-NOTICES.txt +55 -5
- package/dist/types/app-keybindings.d.ts +18 -8
- package/dist/types/apps/git/diff-pane.d.ts +5 -0
- package/dist/types/apps/git/git-tui.d.ts +16 -2
- package/dist/types/apps/git/state.d.ts +11 -0
- package/dist/types/autocomplete.d.ts +2 -0
- package/dist/types/charts/chart-svg.d.ts +7 -13
- package/dist/types/chat/advisor-message.d.ts +4 -1
- package/dist/types/chat/assistant-message.d.ts +9 -4
- package/dist/types/chat/figure-markdown.d.ts +7 -2
- package/dist/types/chat/messages.d.ts +3 -1
- package/dist/types/chat/read-tool-group.d.ts +2 -2
- package/dist/types/chat/table-chart.d.ts +11 -15
- package/dist/types/chat/tool-execution.d.ts +32 -20
- package/dist/types/chat/user-message.d.ts +3 -2
- package/dist/types/chrome/countdown-timer.d.ts +3 -2
- package/dist/types/chrome/diff.d.ts +2 -3
- package/dist/types/chrome/transcript-container.d.ts +2 -0
- package/dist/types/components/image.d.ts +2 -0
- package/dist/types/components/markdown.d.ts +49 -3
- package/dist/types/components/text.d.ts +2 -0
- package/dist/types/debug-server.d.ts +2 -0
- package/dist/types/keybinding-matchers.d.ts +6 -0
- package/dist/types/multiplexers/tmux.d.ts +1 -0
- package/dist/types/multiplexers/types.d.ts +6 -0
- package/dist/types/native/backend.d.ts +5 -4
- package/dist/types/native/blobs.d.ts +24 -12
- package/dist/types/overlays/agent-hub-projection.d.ts +10 -5
- package/dist/types/overlays/annotation-overlay.d.ts +19 -0
- package/dist/types/overlays/approval-dialog.d.ts +79 -0
- package/dist/types/overlays/ask-dialog.d.ts +11 -6
- package/dist/types/overlays/btw-history-panel.d.ts +2 -6
- package/dist/types/overlays/hook-editor.d.ts +7 -2
- package/dist/types/overlays/hook-input.d.ts +12 -4
- package/dist/types/overlays/job-tree.d.ts +50 -0
- package/dist/types/overlays/jobs-panel.d.ts +25 -11
- package/dist/types/overlays/login-dialog.d.ts +8 -3
- package/dist/types/overlays/mcp-add-wizard.d.ts +9 -1
- package/dist/types/overlays/model-browser.d.ts +6 -1
- package/dist/types/overlays/model-picker.d.ts +2 -0
- package/dist/types/overlays/pause-screen.d.ts +5 -0
- package/dist/types/overlays/plan-review-overlay.d.ts +4 -0
- package/dist/types/overlays/report-panel.d.ts +5 -1
- package/dist/types/overlays/reset-usage-selector.d.ts +7 -5
- package/dist/types/overlays/session-account-selector.d.ts +4 -0
- package/dist/types/overlays/session-info-overlay.d.ts +7 -3
- package/dist/types/overlays/session-selector.d.ts +3 -1
- package/dist/types/overlays/usage-dashboard.d.ts +6 -6
- package/dist/types/prompt/composer.d.ts +21 -20
- package/dist/types/prompt/custom-editor.d.ts +20 -10
- package/dist/types/prompt/image-references.d.ts +22 -0
- package/dist/types/prompt/internal-url-autocomplete.d.ts +1 -0
- package/dist/types/prompt/macos-spelling.d.ts +2 -1
- package/dist/types/prompt/prose-gate.d.ts +3 -0
- package/dist/types/prompt/queued-messages.d.ts +1 -1
- package/dist/types/prompt/typing-word.d.ts +25 -0
- package/dist/types/prompt/welcome.d.ts +26 -6
- package/dist/types/render/hanging-wrap.d.ts +7 -0
- package/dist/types/render/hyperlink.d.ts +2 -0
- package/dist/types/render/render-utils.d.ts +9 -1
- package/dist/types/render/utils.d.ts +1 -1
- package/dist/types/setup/scenes/splash.d.ts +6 -5
- package/dist/types/status-line/context-usage.d.ts +4 -1
- package/dist/types/status-line/host.d.ts +6 -4
- package/dist/types/status-line/startup.d.ts +6 -6
- package/dist/types/status-line/types.d.ts +3 -0
- package/dist/types/stdin-buffer.d.ts +7 -0
- package/dist/types/terminal-capabilities.d.ts +16 -1
- package/dist/types/terminal-multiplexer.d.ts +7 -0
- package/dist/types/terminal.d.ts +8 -4
- package/dist/types/theme/symbols.d.ts +1 -1
- package/dist/types/theme/theme.d.ts +11 -0
- package/dist/types/thinking.d.ts +5 -0
- package/dist/types/tmux.d.ts +39 -4
- package/dist/types/tools/ast-edit.d.ts +2 -2
- package/dist/types/tools/ast-grep.d.ts +2 -2
- package/dist/types/tools/bash.d.ts +6 -1
- package/dist/types/tools/edit.d.ts +2 -0
- package/dist/types/tools/eval.d.ts +53 -4
- package/dist/types/tools/find.d.ts +14 -4
- package/dist/types/tools/glob.d.ts +3 -3
- package/dist/types/tools/grep.d.ts +2 -2
- package/dist/types/tools/grouped-file-output.d.ts +9 -4
- package/dist/types/tools/native-view.d.ts +1 -1
- package/dist/types/tools/read.d.ts +32 -10
- package/dist/types/tools/renderer.d.ts +2 -0
- package/dist/types/tools/task.d.ts +31 -0
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/tui.d.ts +20 -3
- package/package.json +10 -10
- package/src/app-keybindings.d.ts +18 -8
- package/src/app-keybindings.ts +20 -8
- package/src/apps/autoresearch-dashboard.ts +444 -27
- package/src/apps/debug/raw-sse.ts +2 -2
- package/src/apps/git/diff-pane.ts +39 -41
- package/src/apps/git/git-tui.ts +80 -14
- package/src/apps/git/sidebar.ts +10 -1
- package/src/apps/git/state.ts +12 -0
- package/src/autocomplete.d.ts +2 -0
- package/src/autocomplete.ts +21 -19
- package/src/bracketed-paste.ts +9 -2
- package/src/charts/chart-svg.d.ts +7 -13
- package/src/charts/chart-svg.ts +19 -22
- package/src/chat/advisor-message.d.ts +4 -1
- package/src/chat/advisor-message.ts +77 -24
- package/src/chat/assistant-message.d.ts +9 -4
- package/src/chat/assistant-message.ts +127 -31
- package/src/chat/bash-execution.ts +7 -2
- package/src/chat/figure-markdown.d.ts +7 -2
- package/src/chat/figure-markdown.ts +10 -3
- package/src/chat/image-loading.ts +9 -6
- package/src/chat/messages.d.ts +3 -1
- package/src/chat/messages.ts +4 -1
- package/src/chat/read-tool-group.d.ts +2 -2
- package/src/chat/read-tool-group.ts +97 -42
- package/src/chat/svg-figure.ts +3 -1
- package/src/chat/svg-source.ts +6 -3
- package/src/chat/table-chart.d.ts +11 -15
- package/src/chat/table-chart.ts +24 -57
- package/src/chat/tool-execution.d.ts +32 -20
- package/src/chat/tool-execution.ts +103 -36
- package/src/chat/user-message.d.ts +3 -2
- package/src/chat/user-message.ts +54 -34
- package/src/chrome/countdown-timer.ts +3 -3
- package/src/chrome/diff.d.ts +2 -3
- package/src/chrome/diff.ts +4 -9
- package/src/chrome/transcript-container.d.ts +2 -0
- package/src/chrome/transcript-container.ts +212 -28
- package/src/components/editor.ts +12 -12
- package/src/components/image.d.ts +2 -0
- package/src/components/image.ts +54 -20
- package/src/components/markdown.d.ts +49 -3
- package/src/components/markdown.ts +461 -11
- package/src/components/settings-list.ts +2 -4
- package/src/components/text.d.ts +2 -0
- package/src/components/text.ts +11 -1
- package/src/debug-server.d.ts +2 -0
- package/src/debug-server.ts +51 -69
- package/src/hotkeys-markdown.ts +2 -0
- package/src/keybinding-matchers.d.ts +6 -0
- package/src/keybinding-matchers.ts +19 -0
- package/src/multiplexers/tmux.d.ts +1 -0
- package/src/multiplexers/tmux.ts +3 -0
- package/src/multiplexers/types.d.ts +6 -0
- package/src/multiplexers/types.ts +6 -0
- package/src/native/backend.d.ts +5 -4
- package/src/native/backend.ts +13 -5
- package/src/native/blobs.d.ts +24 -12
- package/src/native/blobs.ts +56 -7
- package/src/native/reconcile.ts +43 -21
- package/src/overlays/advisor-config.ts +259 -8
- package/src/overlays/agent-hub-projection.d.ts +10 -5
- package/src/overlays/agent-hub-projection.ts +63 -12
- package/src/overlays/annotation-overlay.ts +422 -168
- package/src/overlays/approval-dialog.ts +389 -0
- package/src/overlays/ask-dialog.ts +244 -105
- package/src/overlays/btw-history-panel.ts +208 -147
- package/src/overlays/extensions/extension-list.ts +4 -6
- package/src/overlays/hook-editor.ts +90 -32
- package/src/overlays/hook-input.ts +105 -19
- package/src/overlays/job-tree.ts +177 -0
- package/src/overlays/jobs-panel.ts +222 -142
- package/src/overlays/login-dialog.ts +201 -91
- package/src/overlays/mcp-add-wizard.ts +562 -279
- package/src/overlays/model-browser.ts +32 -7
- package/src/overlays/model-hub.ts +4 -10
- package/src/overlays/model-picker.ts +58 -21
- package/src/overlays/pause-screen.ts +81 -23
- package/src/overlays/plan-review-overlay.ts +16 -0
- package/src/overlays/plugin-selector.ts +17 -1
- package/src/overlays/queue-mode-selector.ts +10 -0
- package/src/overlays/report-panel.ts +6 -2
- package/src/overlays/reset-usage-selector.ts +130 -21
- package/src/overlays/session-account-selector.ts +36 -0
- package/src/overlays/session-info-overlay.ts +199 -76
- package/src/overlays/session-selector.ts +15 -10
- package/src/overlays/settings-selector.ts +3 -0
- package/src/overlays/show-images-selector.ts +1 -0
- package/src/overlays/snapcompact-shape-preview.ts +13 -6
- package/src/overlays/theme-selector.ts +6 -1
- package/src/overlays/thinking-selector.ts +8 -4
- package/src/overlays/tree-selector.ts +117 -2
- package/src/overlays/usage-dashboard.ts +498 -144
- package/src/prompt/composer-cache.ts +1 -1
- package/src/prompt/composer.ts +194 -74
- package/src/prompt/custom-editor.ts +168 -65
- package/src/prompt/image-references.d.ts +22 -0
- package/src/prompt/image-references.ts +79 -2
- package/src/prompt/internal-url-autocomplete.ts +1 -0
- package/src/prompt/macos-spelling.ts +28 -5
- package/src/prompt/prose-gate.ts +3 -0
- package/src/prompt/queued-messages.ts +9 -2
- package/src/prompt/tips.txt +1 -2
- package/src/prompt/typing-word.ts +59 -0
- package/src/prompt/welcome.ts +97 -45
- package/src/render/code-cell.ts +18 -5
- package/src/render/hanging-wrap.d.ts +7 -0
- package/src/render/hanging-wrap.ts +58 -0
- package/src/render/hyperlink.d.ts +2 -0
- package/src/render/hyperlink.ts +5 -0
- package/src/render/render-utils.d.ts +9 -1
- package/src/render/render-utils.ts +16 -2
- package/src/render/utils.d.ts +1 -1
- package/src/render/utils.ts +1 -1
- package/src/render/width-aware-text.ts +6 -5
- package/src/setup/scenes/splash.ts +22 -8
- package/src/status-line/component.ts +23 -10
- package/src/status-line/context-usage.ts +176 -73
- package/src/status-line/footer.ts +18 -35
- package/src/status-line/host.ts +6 -1
- package/src/status-line/segments.ts +16 -5
- package/src/status-line/startup.ts +6 -6
- package/src/status-line/types.ts +1 -0
- package/src/stdin-buffer.d.ts +7 -0
- package/src/stdin-buffer.ts +11 -1
- package/src/terminal-capabilities.d.ts +16 -1
- package/src/terminal-capabilities.ts +66 -28
- package/src/terminal-multiplexer.d.ts +7 -0
- package/src/terminal-multiplexer.ts +14 -0
- package/src/terminal.d.ts +8 -4
- package/src/terminal.ts +160 -46
- package/src/theme/symbols.d.ts +1 -1
- package/src/theme/symbols.ts +1 -0
- package/src/theme/theme-class.ts +1 -0
- package/src/theme/theme.d.ts +11 -0
- package/src/theme/theme.ts +46 -14
- package/src/thinking.d.ts +5 -0
- package/src/thinking.ts +9 -1
- package/src/tmux.d.ts +39 -4
- package/src/tmux.ts +110 -16
- package/src/tools/ask.ts +20 -9
- package/src/tools/ast-edit.d.ts +2 -2
- package/src/tools/ast-edit.ts +30 -7
- package/src/tools/ast-grep.d.ts +2 -2
- package/src/tools/ast-grep.ts +39 -35
- package/src/tools/bash.d.ts +6 -1
- package/src/tools/bash.ts +15 -9
- package/src/tools/debug.ts +2 -1
- package/src/tools/default-renderer.ts +3 -2
- package/src/tools/edit.d.ts +2 -0
- package/src/tools/edit.ts +100 -21
- package/src/tools/eval.d.ts +53 -4
- package/src/tools/eval.ts +173 -137
- package/src/tools/find.d.ts +14 -4
- package/src/tools/find.ts +71 -18
- package/src/tools/glob.d.ts +3 -3
- package/src/tools/glob.ts +51 -17
- package/src/tools/grep.d.ts +2 -2
- package/src/tools/grep.ts +99 -22
- package/src/tools/grouped-file-output.d.ts +9 -4
- package/src/tools/grouped-file-output.ts +21 -6
- package/src/tools/lsp.ts +30 -18
- package/src/tools/mcp.ts +2 -2
- package/src/tools/native-view.d.ts +1 -1
- package/src/tools/native-view.ts +2 -2
- package/src/tools/read.d.ts +32 -10
- package/src/tools/read.ts +89 -43
- package/src/tools/renderer.d.ts +2 -0
- package/src/tools/renderer.ts +2 -0
- package/src/tools/task.d.ts +31 -0
- package/src/tools/task.ts +40 -7
- package/src/tools/todo.ts +3 -2
- package/src/tools/wait.ts +16 -30
- package/src/tools/web-search.ts +2 -1
- package/src/tools/write.d.ts +2 -0
- package/src/tools/write.ts +36 -12
- package/src/tools/yield.ts +2 -2
- package/src/tui.d.ts +20 -3
- package/src/tui.ts +140 -36
- package/src/utils.ts +5 -3
- package/dist/types/tools/eval-format/code-mode-declarations.d.ts +0 -5
- package/src/tools/eval-format/code-mode-declarations.ts +0 -61
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
skillToken,
|
|
40
40
|
} from "./composer-attachments";
|
|
41
41
|
import { type MacOSSpellingFeatures, MacOSSpellingProvider } from "./macos-spelling";
|
|
42
|
+
import { TypingWordTracker } from "./typing-word";
|
|
42
43
|
import { hasMagicKeyword, highlightMagicKeywords, magicKeywordRanges } from "./magic-keywords";
|
|
43
44
|
import type { TspEditorDecoration, TspText } from "@linxiraos/pi-wire";
|
|
44
45
|
import { isNativeRendering } from "../native/state";
|
|
@@ -64,6 +65,8 @@ export interface ComposerNativeState {
|
|
|
64
65
|
readonly rate?: number;
|
|
65
66
|
/** A turn is running: the send keycap becomes a Stop button. */
|
|
66
67
|
readonly running: boolean;
|
|
68
|
+
/** Native dictation control; undefined hides it for hosts without speech-to-text. */
|
|
69
|
+
readonly dictation?: "idle" | "starting" | "recording" | "transcribing";
|
|
67
70
|
/** The viewed subagent's lineage, outermost first and the viewed agent last; undefined on the main session. */
|
|
68
71
|
readonly viewing?: readonly string[];
|
|
69
72
|
/** The session's title, quoted in italics as the TSP placeholder; undefined until it has one. */
|
|
@@ -75,12 +78,18 @@ const FOCUS_ACTION = "focus:";
|
|
|
75
78
|
|
|
76
79
|
const IDLE_COMPOSER: ComposerNativeState = { running: false };
|
|
77
80
|
|
|
81
|
+
/** Recording and transcription still write into the draft, so sending it then would split the dictation. */
|
|
82
|
+
function dictationHoldsDraft(dictation: ComposerNativeState["dictation"]): boolean {
|
|
83
|
+
return dictation === "recording" || dictation === "transcribing";
|
|
84
|
+
}
|
|
85
|
+
|
|
78
86
|
/** The composer's controls around the input, rebuilt when what they show changes; the bar adds the rate and facts. */
|
|
79
87
|
interface ComposerControls {
|
|
80
88
|
readonly focus: NativeNode | undefined;
|
|
81
89
|
readonly mode: NativeNode | undefined;
|
|
82
90
|
readonly model: NativeNode | undefined;
|
|
83
91
|
readonly effort: NativeNode | undefined;
|
|
92
|
+
readonly dictation: NativeNode | undefined;
|
|
84
93
|
readonly submit: NativeNode;
|
|
85
94
|
}
|
|
86
95
|
|
|
@@ -453,6 +462,8 @@ export type ComposerChipDescriptor =
|
|
|
453
462
|
*/
|
|
454
463
|
export class CustomEditor extends Editor {
|
|
455
464
|
#spelling = new MacOSSpellingProvider();
|
|
465
|
+
/** The word being typed: its typo mark waits until the caret leaves it. */
|
|
466
|
+
#typing = new TypingWordTracker();
|
|
456
467
|
#wordCompletion = new WordCompletionProvider();
|
|
457
468
|
imageLinks?: readonly (string | undefined)[];
|
|
458
469
|
|
|
@@ -857,6 +868,9 @@ export class CustomEditor extends Editor {
|
|
|
857
868
|
}
|
|
858
869
|
// One string instance per revision: whole-buffer memos downstream hit on identity.
|
|
859
870
|
const editorText = this.#decorationText;
|
|
871
|
+
const caret = this.getCursor();
|
|
872
|
+
this.#typing.update(this.textRevision, this.#decorationLines, caret.line, caret.col, this.focused);
|
|
873
|
+
const typing = this.#typing.word;
|
|
860
874
|
const animated = this.focused && this.#shimmerEnabled() && hasMagicKeyword(editorText);
|
|
861
875
|
const phase = animated ? (Date.now() % CustomEditor.SHIMMER_PERIOD_MS) / CustomEditor.SHIMMER_PERIOD_MS : 0;
|
|
862
876
|
if (animated) this.#scheduleShimmerFrame();
|
|
@@ -879,6 +893,7 @@ export class CustomEditor extends Editor {
|
|
|
879
893
|
lines: this.#decorationLines,
|
|
880
894
|
line: context.line,
|
|
881
895
|
startCol: context.startCol + sourceOffset,
|
|
896
|
+
typing,
|
|
882
897
|
},
|
|
883
898
|
span => highlightMagicKeywords(span, undefined, phase),
|
|
884
899
|
);
|
|
@@ -926,6 +941,15 @@ export class CustomEditor extends Editor {
|
|
|
926
941
|
);
|
|
927
942
|
};
|
|
928
943
|
|
|
944
|
+
/** Catch the typed word up with the caret first: its leaving the word (or a blur) re-describes the typo marks. */
|
|
945
|
+
override describe(cx: DescribeContext): NativeNode {
|
|
946
|
+
const caret = this.getCursor();
|
|
947
|
+
if (this.#typing.update(this.textRevision, this.getLines(), caret.line, caret.col, this.focused)) {
|
|
948
|
+
this.invalidate();
|
|
949
|
+
}
|
|
950
|
+
return super.describe(cx);
|
|
951
|
+
}
|
|
952
|
+
|
|
929
953
|
/**
|
|
930
954
|
* TSP decorations: magic keywords shimmer (terminal-clocked) in the accent
|
|
931
955
|
* color, chips and markers take their token colors, misspellings are marked,
|
|
@@ -966,6 +990,7 @@ export class CustomEditor extends Editor {
|
|
|
966
990
|
|
|
967
991
|
const queueBody = parseQueueShorthand(text);
|
|
968
992
|
const queueList = queueBody !== undefined && isQueuedMessageList(queueBody);
|
|
993
|
+
const typing = this.#typing.word;
|
|
969
994
|
let lineStart = 0;
|
|
970
995
|
for (let line = 0; line < lines.length; line++) {
|
|
971
996
|
const value = lines[line] ?? "";
|
|
@@ -979,7 +1004,8 @@ export class CustomEditor extends Editor {
|
|
|
979
1004
|
decor.push({ from: lineStart + indent, to: lineStart + marker[0].length, s: "accent" });
|
|
980
1005
|
}
|
|
981
1006
|
}
|
|
982
|
-
|
|
1007
|
+
const context = { editorText: text, lines, line, startCol: 0, typing };
|
|
1008
|
+
for (const typo of this.#spelling.typoRanges(value, context)) {
|
|
983
1009
|
decor.push({ from: lineStart + typo.start, to: lineStart + typo.start + typo.length, s: "typo" });
|
|
984
1010
|
}
|
|
985
1011
|
lineStart += value.length + 1;
|
|
@@ -1034,9 +1060,10 @@ export class CustomEditor extends Editor {
|
|
|
1034
1060
|
}, CustomEditor.SHIMMER_FRAME_MS);
|
|
1035
1061
|
this.#shimmerTimer.unref?.();
|
|
1036
1062
|
}
|
|
1037
|
-
/** Editing is available during bootstrap; atomic sends wait until submission is wired and enabled
|
|
1063
|
+
/** Editing is available during bootstrap; atomic sends wait until submission is wired and enabled
|
|
1064
|
+
* and no dictation is still writing into the draft. */
|
|
1038
1065
|
protected override get nativeSendable(): boolean {
|
|
1039
|
-
return this.onSubmit !== undefined && !this.disableSubmit;
|
|
1066
|
+
return this.onSubmit !== undefined && !this.disableSubmit && !dictationHoldsDraft(this.composerState().dictation);
|
|
1040
1067
|
}
|
|
1041
1068
|
/** The quoted-title placeholder, kept while the title stands so the editor node stays reused. */
|
|
1042
1069
|
#titlePlaceholder: { title: string; text: TspText } | undefined;
|
|
@@ -1078,9 +1105,9 @@ export class CustomEditor extends Editor {
|
|
|
1078
1105
|
* subagent is focused, the attachment chips, a `line` row of the
|
|
1079
1106
|
* shell-mode chip and the input, and the `bar`: model chip, effort chip
|
|
1080
1107
|
* (or the effort glyph as the model chip's icon), the generation rate,
|
|
1081
|
-
* the other status facts, usage, then send (Stop while a turn runs).
|
|
1082
|
-
* Clicks come back as `status.model`, `thinking.cycle`, `
|
|
1083
|
-
* `interrupt` and `focus:<id>` actions.
|
|
1108
|
+
* the other status facts, usage, dictation, then send (Stop while a turn runs).
|
|
1109
|
+
* Clicks come back as `status.model`, `thinking.cycle`, `dictation.toggle`,
|
|
1110
|
+
* `submit`, `interrupt` and `focus:<id>` actions.
|
|
1084
1111
|
*/
|
|
1085
1112
|
override describeLayout = (input: NativeNode, cx: DescribeContext): NativeEditorLayout => {
|
|
1086
1113
|
const effortGlyph = cx.supports("effort");
|
|
@@ -1095,6 +1122,7 @@ export class CustomEditor extends Editor {
|
|
|
1095
1122
|
shell?.excluded,
|
|
1096
1123
|
state.thinking,
|
|
1097
1124
|
state.running,
|
|
1125
|
+
state.dictation,
|
|
1098
1126
|
thinkingKey,
|
|
1099
1127
|
modelKey,
|
|
1100
1128
|
interruptKey,
|
|
@@ -1138,6 +1166,7 @@ export class CustomEditor extends Editor {
|
|
|
1138
1166
|
// The status facts are the bar's flexible space; without them a spacer keeps send at the end.
|
|
1139
1167
|
facts?.extras ?? node("row", { grow: 1 }, [], "gap"),
|
|
1140
1168
|
facts?.usage,
|
|
1169
|
+
controls.dictation,
|
|
1141
1170
|
controls.submit,
|
|
1142
1171
|
]),
|
|
1143
1172
|
{ role: "zeta.composer.bar", gap: "sm", align: "center" },
|
|
@@ -1289,6 +1318,38 @@ export class CustomEditor extends Editor {
|
|
|
1289
1318
|
"effort",
|
|
1290
1319
|
)
|
|
1291
1320
|
: undefined;
|
|
1321
|
+
const recording = state.dictation === "recording";
|
|
1322
|
+
const starting = state.dictation === "starting";
|
|
1323
|
+
const transcribing = state.dictation === "transcribing";
|
|
1324
|
+
const dictation =
|
|
1325
|
+
state.dictation === undefined
|
|
1326
|
+
? undefined
|
|
1327
|
+
: node(
|
|
1328
|
+
"row",
|
|
1329
|
+
{
|
|
1330
|
+
role: "zeta.composer.dictation",
|
|
1331
|
+
gap: "xs",
|
|
1332
|
+
align: "center",
|
|
1333
|
+
tone: recording ? "error" : starting || transcribing ? "pending" : undefined,
|
|
1334
|
+
title: recording
|
|
1335
|
+
? "Stop recording and transcribe"
|
|
1336
|
+
: starting
|
|
1337
|
+
? "Cancel dictation"
|
|
1338
|
+
: transcribing
|
|
1339
|
+
? "Transcribing dictation"
|
|
1340
|
+
: "Start dictation",
|
|
1341
|
+
// The model can take a while to load on first use, so a start stays cancellable.
|
|
1342
|
+
actions: transcribing ? undefined : { click: "dictation.toggle" },
|
|
1343
|
+
},
|
|
1344
|
+
starting || transcribing
|
|
1345
|
+
? [node("spinner", { label: starting ? "Starting…" : "Transcribing…" }, undefined, "pending")]
|
|
1346
|
+
: compact([
|
|
1347
|
+
node("icon", { name: recording ? "stop" : "mic" }, undefined, "icon"),
|
|
1348
|
+
recording && node("text", { text: "Recording · Stop", wrap: "none" }, undefined, "label"),
|
|
1349
|
+
]),
|
|
1350
|
+
"dictation",
|
|
1351
|
+
);
|
|
1352
|
+
const dictating = dictationHoldsDraft(state.dictation);
|
|
1292
1353
|
const submit = state.running
|
|
1293
1354
|
? node(
|
|
1294
1355
|
"text",
|
|
@@ -1307,13 +1368,14 @@ export class CustomEditor extends Editor {
|
|
|
1307
1368
|
{
|
|
1308
1369
|
role: "zeta.composer.send",
|
|
1309
1370
|
keys: ["enter"],
|
|
1310
|
-
title: `Send ${formatTooltipKey("enter")}`,
|
|
1311
|
-
|
|
1371
|
+
title: dictating ? "Finish dictation before sending" : `Send ${formatTooltipKey("enter")}`,
|
|
1372
|
+
tone: dictating ? "muted" : undefined,
|
|
1373
|
+
actions: dictating ? undefined : { click: "submit" },
|
|
1312
1374
|
},
|
|
1313
1375
|
undefined,
|
|
1314
1376
|
"send",
|
|
1315
1377
|
);
|
|
1316
|
-
if (!shell) return { focus, mode: undefined, model, effort, submit };
|
|
1378
|
+
if (!shell) return { focus, mode: undefined, model, effort, dictation, submit };
|
|
1317
1379
|
const runs = shell.kind === "bash" ? "Runs in your shell" : "Runs in Python";
|
|
1318
1380
|
const mode = keyed(
|
|
1319
1381
|
row(
|
|
@@ -1336,15 +1398,15 @@ export class CustomEditor extends Editor {
|
|
|
1336
1398
|
),
|
|
1337
1399
|
"mode",
|
|
1338
1400
|
);
|
|
1339
|
-
return { focus, mode, model, effort, submit };
|
|
1401
|
+
return { focus, mode, model, effort, dictation, submit };
|
|
1340
1402
|
}
|
|
1341
1403
|
|
|
1342
1404
|
/**
|
|
1343
|
-
*
|
|
1344
|
-
*
|
|
1345
|
-
* the status facts' clicks
|
|
1346
|
-
* go to the buffer; `send` submits its own prompt after
|
|
1347
|
-
* for recall and waiting for in-flight clipboard work.
|
|
1405
|
+
* Dictation clicks go directly to the host, independent of keybindings.
|
|
1406
|
+
* Other controls take the same paths as their keys; the viewing header's
|
|
1407
|
+
* links (`focus:<id>`) go to the host, the status facts' clicks to their source.
|
|
1408
|
+
* Selection edits go to the buffer; `send` submits its own prompt after
|
|
1409
|
+
* saving the old draft for recall and waiting for in-flight clipboard work.
|
|
1348
1410
|
*/
|
|
1349
1411
|
override handleNativeEvent(event: NativeUiEvent): void {
|
|
1350
1412
|
if (event.type === "send") {
|
|
@@ -1368,8 +1430,13 @@ export class CustomEditor extends Editor {
|
|
|
1368
1430
|
case "thinking.cycle":
|
|
1369
1431
|
this.onCycleThinkingLevel?.();
|
|
1370
1432
|
return;
|
|
1433
|
+
case "dictation.toggle": {
|
|
1434
|
+
const { dictation } = this.composerState();
|
|
1435
|
+
if (dictation !== undefined && dictation !== "transcribing") this.onToggleDictation?.();
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1371
1438
|
case "submit":
|
|
1372
|
-
this.submit();
|
|
1439
|
+
if (this.nativeSendable) this.submit();
|
|
1373
1440
|
return;
|
|
1374
1441
|
case "interrupt":
|
|
1375
1442
|
this.onEscape?.();
|
|
@@ -1404,6 +1471,8 @@ export class CustomEditor extends Editor {
|
|
|
1404
1471
|
onRetry?: () => void;
|
|
1405
1472
|
/** Called when Caps Lock is pressed. */
|
|
1406
1473
|
onCapsLock?: () => void;
|
|
1474
|
+
/** Called directly by the native microphone/stop control, without synthesizing a keypress. */
|
|
1475
|
+
onToggleDictation?: () => void;
|
|
1407
1476
|
/** Called when left-arrow is pressed while the editor is empty (cursor necessarily at start). */
|
|
1408
1477
|
onLeftAtStart?: () => void;
|
|
1409
1478
|
/** Called when the viewing header asks to view agent `id` ({@link MAIN_AGENT_ID}: the main session). */
|
|
@@ -1436,8 +1505,9 @@ export class CustomEditor extends Editor {
|
|
|
1436
1505
|
* `pendingImages` (Codex PR #3602 review). */
|
|
1437
1506
|
#pasteInFlight = 0;
|
|
1438
1507
|
/** Input chunks and explicit prompts deferred behind an in-flight paste,
|
|
1439
|
-
* drained in FIFO order once the paste count returns to zero.
|
|
1440
|
-
|
|
1508
|
+
* drained in FIFO order once the paste count returns to zero. A `draft` chunk
|
|
1509
|
+
* arrived through {@link handleDraftEdit} and drains back through it. */
|
|
1510
|
+
#pendingInput: (string | { draft: string } | Extract<NativeUiEvent, { type: "send" }>)[] = [];
|
|
1441
1511
|
#actionKeys = new Map<ConfigurableEditorAction, KeyId[]>(
|
|
1442
1512
|
Object.entries(DEFAULT_ACTION_KEYS).map(([action, keys]) => [action as ConfigurableEditorAction, [...keys]]),
|
|
1443
1513
|
);
|
|
@@ -1516,6 +1586,7 @@ export class CustomEditor extends Editor {
|
|
|
1516
1586
|
const drained = this.#pendingInput.splice(0);
|
|
1517
1587
|
for (const input of drained) {
|
|
1518
1588
|
if (typeof input === "string") this.handleInput(input);
|
|
1589
|
+
else if ("draft" in input) this.handleDraftEdit(input.draft);
|
|
1519
1590
|
else this.handleNativeEvent(input);
|
|
1520
1591
|
}
|
|
1521
1592
|
}
|
|
@@ -1530,6 +1601,11 @@ export class CustomEditor extends Editor {
|
|
|
1530
1601
|
void promise.then(this.#onPasteSettled, this.#onPasteSettled);
|
|
1531
1602
|
}
|
|
1532
1603
|
|
|
1604
|
+
/** True while a clipboard-image or image-path paste is still landing; later keys queue behind it. */
|
|
1605
|
+
get pasteInFlight(): boolean {
|
|
1606
|
+
return this.#pasteInFlight > 0;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1533
1609
|
capturesInput(data: string): boolean {
|
|
1534
1610
|
return this.spaceHold.shouldRoute(data);
|
|
1535
1611
|
}
|
|
@@ -1550,56 +1626,21 @@ export class CustomEditor extends Editor {
|
|
|
1550
1626
|
this.onCapsLock();
|
|
1551
1627
|
return;
|
|
1552
1628
|
}
|
|
1553
|
-
|
|
1554
|
-
// Bracketed-paste assembly. Some terminals fragment the start marker,
|
|
1555
|
-
// the payload, and the end marker across separate stdin chunks
|
|
1556
|
-
// (Windows Terminal under heavy load, certain SSH muxes, …); the
|
|
1557
|
-
// inherited handler then sees a zero-length payload and silently
|
|
1558
|
-
// drops it through the normal text-insert path. Running our own
|
|
1559
|
-
// `BracketedPasteHandler` ahead of `super.handleInput` lets us route
|
|
1560
|
-
// the assembled content regardless of chunk boundaries:
|
|
1561
|
-
// - empty payload → `onPasteImage` (#3601: `Cmd+V`/`Ctrl+V` on an
|
|
1562
|
-
// image-only macOS pasteboard the terminal stripped to `""` first);
|
|
1563
|
-
// - explicit image-file paths → `onPasteImagePath` (#3506);
|
|
1564
|
-
// - anything else → the base editor's `pasteText` so `[Paste #N]`
|
|
1565
|
-
// markers, autocomplete, and undo state stay intact.
|
|
1566
|
-
const paste = this.#pasteHandler.process(data);
|
|
1567
|
-
if (paste.handled) {
|
|
1568
|
-
if (paste.pasteContent === undefined) return; // still buffering — wait for end marker
|
|
1569
|
-
const content = paste.pasteContent;
|
|
1570
|
-
const remaining = paste.remaining;
|
|
1571
|
-
// Queue any trailing bytes from the same read (typically a follow-up keystroke such as
|
|
1572
|
-
// Enter that the user pressed right after Cmd+V) so they only fire *after* the paste
|
|
1573
|
-
// completes — fixes the race where submit runs against an empty `pendingImages`.
|
|
1574
|
-
if (remaining.length > 0) this.#pendingInput.push(remaining);
|
|
1575
|
-
if (content.length === 0 && this.onPasteImage) {
|
|
1576
|
-
this.#trackAsyncPaste(Promise.resolve(this.onPasteImage()));
|
|
1577
|
-
return;
|
|
1578
|
-
}
|
|
1579
|
-
const attachmentPaths = extractImagePastePathsFromText(content);
|
|
1580
|
-
if (attachmentPaths && this.onPasteImagePath) {
|
|
1581
|
-
this.#trackAsyncPaste(
|
|
1582
|
-
(async () => {
|
|
1583
|
-
for (const p of attachmentPaths) await this.onPasteImagePath?.(p);
|
|
1584
|
-
})(),
|
|
1585
|
-
);
|
|
1586
|
-
return;
|
|
1587
|
-
}
|
|
1588
|
-
// A submit key that shared the read (see `StdinBuffer`'s paste event) is
|
|
1589
|
-
// about to be drained below, so a large-paste host must stage the paste
|
|
1590
|
-
// synchronously instead of opening a menu the submit would land in.
|
|
1591
|
-
if (this.#isSubmitKey(remaining)) this.pasteText(content, { submitAfterPaste: true });
|
|
1592
|
-
else this.pasteText(content);
|
|
1593
|
-
this.#collapseSkillTokens();
|
|
1594
|
-
this.#collapseModelMentions();
|
|
1595
|
-
// No async paste was started; drain the queued trailing bytes ourselves.
|
|
1596
|
-
this.#drainPendingInput();
|
|
1597
|
-
return;
|
|
1598
|
-
}
|
|
1629
|
+
if (this.#routeBracketedPaste(data, false)) return;
|
|
1599
1630
|
|
|
1600
1631
|
const parsedKey = parseKey(data);
|
|
1601
1632
|
const canonical = parsedKey !== undefined ? canonicalKeyId(parsedKey) : undefined;
|
|
1602
1633
|
|
|
1634
|
+
// Enter matches the disabled Send button while dictation still writes into the draft;
|
|
1635
|
+
// dictation's own submit trigger goes through submit(), not this key path.
|
|
1636
|
+
if (
|
|
1637
|
+
canonical !== undefined &&
|
|
1638
|
+
getKeybindings().matchesCanonical(canonical, "tui.input.submit") &&
|
|
1639
|
+
dictationHoldsDraft(this.composerState().dictation)
|
|
1640
|
+
) {
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1603
1644
|
// Space-hold push-to-talk runs before editor shortcuts so a reserved binding can never
|
|
1604
1645
|
// delete, move, submit, or invoke an app action while the gesture is enabled.
|
|
1605
1646
|
const spaceHoldText = getSpaceHoldText(data, canonical);
|
|
@@ -1828,6 +1869,57 @@ export class CustomEditor extends Editor {
|
|
|
1828
1869
|
this.#normalizeQueuePrefix(hadBareQueuePrefix);
|
|
1829
1870
|
}
|
|
1830
1871
|
|
|
1872
|
+
/**
|
|
1873
|
+
* Bracketed-paste assembly. Some terminals fragment the start marker,
|
|
1874
|
+
* the payload, and the end marker across separate stdin chunks
|
|
1875
|
+
* (Windows Terminal under heavy load, certain SSH muxes, …); the
|
|
1876
|
+
* inherited handler then sees a zero-length payload and silently
|
|
1877
|
+
* drops it through the normal text-insert path. Running our own
|
|
1878
|
+
* `BracketedPasteHandler` ahead of `super.handleInput` lets us route
|
|
1879
|
+
* the assembled content regardless of chunk boundaries:
|
|
1880
|
+
* - empty payload → `onPasteImage` (#3601: `Cmd+V`/`Ctrl+V` on an
|
|
1881
|
+
* image-only macOS pasteboard the terminal stripped to `""` first);
|
|
1882
|
+
* - explicit image-file paths → `onPasteImagePath` (#3506);
|
|
1883
|
+
* - anything else → the base editor's `pasteText` so `[Paste #N]`
|
|
1884
|
+
* markers, autocomplete, and undo state stay intact.
|
|
1885
|
+
* True when `data` was consumed. `draft` queues trailing bytes back through
|
|
1886
|
+
* {@link handleDraftEdit} instead of {@link handleInput}.
|
|
1887
|
+
*/
|
|
1888
|
+
#routeBracketedPaste(data: string, draft: boolean): boolean {
|
|
1889
|
+
const paste = this.#pasteHandler.process(data);
|
|
1890
|
+
if (!paste.handled) return false;
|
|
1891
|
+
if (paste.pasteContent === undefined) return true; // still buffering — wait for end marker
|
|
1892
|
+
const content = paste.pasteContent;
|
|
1893
|
+
const remaining = paste.remaining;
|
|
1894
|
+
// Queue any trailing bytes from the same read (typically a follow-up keystroke such as
|
|
1895
|
+
// Enter that the user pressed right after Cmd+V) so they only fire *after* the paste
|
|
1896
|
+
// completes — fixes the race where submit runs against an empty `pendingImages`.
|
|
1897
|
+
if (remaining.length > 0) this.#pendingInput.push(draft ? { draft: remaining } : remaining);
|
|
1898
|
+
if (content.length === 0 && this.onPasteImage) {
|
|
1899
|
+
this.#trackAsyncPaste(Promise.resolve(this.onPasteImage()));
|
|
1900
|
+
return true;
|
|
1901
|
+
}
|
|
1902
|
+
const attachmentPaths = extractImagePastePathsFromText(content);
|
|
1903
|
+
if (attachmentPaths && this.onPasteImagePath) {
|
|
1904
|
+
this.#trackAsyncPaste(
|
|
1905
|
+
(async () => {
|
|
1906
|
+
for (const p of attachmentPaths) await this.onPasteImagePath?.(p);
|
|
1907
|
+
})(),
|
|
1908
|
+
);
|
|
1909
|
+
return true;
|
|
1910
|
+
}
|
|
1911
|
+
// A submit key that shared the read (see `StdinBuffer`'s paste event) is
|
|
1912
|
+
// about to be drained below, so a large-paste host must stage the paste
|
|
1913
|
+
// synchronously instead of opening a menu the submit would land in.
|
|
1914
|
+
if (this.#isSubmitKey(remaining)) this.pasteText(content, { submitAfterPaste: true });
|
|
1915
|
+
else this.pasteText(content);
|
|
1916
|
+
this.#collapseSkillTokens();
|
|
1917
|
+
this.#collapseModelMentions();
|
|
1918
|
+
// No async paste was started; drain the queued trailing bytes ourselves.
|
|
1919
|
+
this.#drainPendingInput();
|
|
1920
|
+
return true;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1831
1923
|
/** Promote a newly formed bare `->` / `=>` prefix to a reserved header line by opening the
|
|
1832
1924
|
* queue body beneath it. `hadBareQueuePrefix` is the pre-edit state: a prompt that was
|
|
1833
1925
|
* already just the prefix is left alone so the user can keep editing it. */
|
|
@@ -1846,9 +1938,16 @@ export class CustomEditor extends Editor {
|
|
|
1846
1938
|
* an Ask dialog opened over a non-empty prompt — so finishing or submitting
|
|
1847
1939
|
* the draft cannot fire an editor-slot shortcut that clears
|
|
1848
1940
|
* `editorContainer` and orphans the overlay. Only text editing, cursor
|
|
1849
|
-
* movement, submission,
|
|
1941
|
+
* movement, submission, the clear action, and pastes (clipboard images and
|
|
1942
|
+
* image paths attach as on the composer) reach the buffer.
|
|
1850
1943
|
*/
|
|
1851
1944
|
handleDraftEdit(data: string): void {
|
|
1945
|
+
// Same serialization as handleInput: keys typed behind an image paste wait for it.
|
|
1946
|
+
if (this.#pasteInFlight > 0) {
|
|
1947
|
+
this.#pendingInput.push({ draft: data });
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
if (this.#routeBracketedPaste(data, true)) return;
|
|
1852
1951
|
// The base editor reserves Ctrl+C for parent handling and returns without
|
|
1853
1952
|
// touching the buffer, so the configured clear action must be dispatched
|
|
1854
1953
|
// explicitly here — otherwise the guard's "finish or clear the prompt"
|
|
@@ -1862,6 +1961,10 @@ export class CustomEditor extends Editor {
|
|
|
1862
1961
|
else this.setText("");
|
|
1863
1962
|
return;
|
|
1864
1963
|
}
|
|
1964
|
+
if (canonical !== undefined && this.#matchesAction(canonical, "app.clipboard.pasteImage") && this.onPasteImage) {
|
|
1965
|
+
this.#trackAsyncPaste(Promise.resolve(this.onPasteImage()));
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1865
1968
|
this.#forwardInput(data);
|
|
1866
1969
|
}
|
|
1867
1970
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ImageContent } from "@linxiraos/pi-ai";
|
|
2
|
+
import type { TspSpan } from "@linxiraos/pi-wire";
|
|
2
3
|
/** Materialized image destination returned by the host blob writer. */
|
|
3
4
|
interface ImageBlobResult {
|
|
4
5
|
displayPath: string;
|
|
@@ -23,4 +24,25 @@ type ImageBlobWriterSync = (data: Buffer, options?: {
|
|
|
23
24
|
export declare function imageReferenceHyperlink(label: string, index: number, imageLinks: readonly (string | undefined)[] | undefined, renderLabel: (text: string) => string): string;
|
|
24
25
|
export declare function materializeImageReferenceLinks(images: readonly ImageContent[] | undefined, putBlob: ImageBlobWriter): Promise<(string | undefined)[] | undefined>;
|
|
25
26
|
export declare function materializeImageReferenceLinksSync(images: readonly ImageContent[] | undefined, putBlob: ImageBlobWriterSync): (string | undefined)[] | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Register the host's blob writer that backs links for images without a file of their
|
|
29
|
+
* own. Returns a disposer that unregisters it unless another writer replaced it since.
|
|
30
|
+
*/
|
|
31
|
+
export declare function setImageBlobWriter(next: ImageBlobWriterSync): () => void;
|
|
32
|
+
/**
|
|
33
|
+
* Whether transcript images carry links under the current `tui.hyperlinks` policy: a
|
|
34
|
+
* native surface honours only an explicit `off`, text mode the resolved OSC 8 support.
|
|
35
|
+
* Renderers caching a linked image key their cache on it.
|
|
36
|
+
*/
|
|
37
|
+
export declare function imageLinksEnabled(): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The file a transcript image opens: `sourcePath` when it was read from one, else a
|
|
40
|
+
* blob copy of its bytes written through the host. Undefined when links are disabled
|
|
41
|
+
* or no file exists, so a disabled policy never writes blobs.
|
|
42
|
+
*/
|
|
43
|
+
export declare function transcriptImageLink(image: Pick<ImageContent, "data" | "mimeType">, sourcePath?: string): string | undefined;
|
|
44
|
+
/** `text` (an image's placeholder) as an OSC 8 link to the image's file; plain when hyperlinks are off or no file exists. */
|
|
45
|
+
export declare function linkedImageText(text: string, image: Pick<ImageContent, "data" | "mimeType">, sourcePath?: string): string;
|
|
46
|
+
/** Native counterpart of {@link linkedImageText}: a span linking to the image's file when one exists. */
|
|
47
|
+
export declare function linkedImageSpan(text: string, image: Pick<ImageContent, "data" | "mimeType">, sourcePath: string | undefined, s?: string): TspSpan;
|
|
26
48
|
export {};
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import type { ImageContent } from "@linxiraos/pi-ai";
|
|
2
2
|
import { logger } from "@linxiraos/pi-utils";
|
|
3
|
+
import type { TspSpan } from "@linxiraos/pi-wire";
|
|
4
|
+
import { span } from "../native/describe";
|
|
5
|
+
import { isNativeRendering } from "../native/state";
|
|
6
|
+
import { fileHyperlink, fileLinkSpan, isHyperlinkEnabled, isNativeLinkEnabled } from "../render/hyperlink";
|
|
3
7
|
import { blobExtensionForImageMimeType } from "./image-format";
|
|
4
8
|
|
|
5
9
|
/** Materialized image destination returned by the host blob writer. */
|
|
6
10
|
interface ImageBlobResult {
|
|
7
11
|
displayPath: string;
|
|
8
12
|
}
|
|
9
|
-
import { fileHyperlink } from "../render/hyperlink";
|
|
10
|
-
|
|
11
13
|
/** Probed pixel dimensions riding on the draft image object itself; `null` records a failed
|
|
12
14
|
* probe so the chips band never re-decodes a corrupt header every frame. */
|
|
13
15
|
const kImageDims = Symbol("zeta.imageDimensions");
|
|
@@ -100,3 +102,78 @@ export function materializeImageReferenceLinksSync(
|
|
|
100
102
|
const links = images.map((image, index) => materializeImageReferenceLink(image, index + 1, putBlob));
|
|
101
103
|
return links.some(link => link !== undefined) ? links : undefined;
|
|
102
104
|
}
|
|
105
|
+
|
|
106
|
+
let hostBlobWriter: ImageBlobWriterSync | undefined;
|
|
107
|
+
/** Blob paths already written for an image payload, keyed by mime type and content hash. */
|
|
108
|
+
const blobLinks = new Map<string, string>();
|
|
109
|
+
const BLOB_LINK_CACHE_LIMIT = 256;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Register the host's blob writer that backs links for images without a file of their
|
|
113
|
+
* own. Returns a disposer that unregisters it unless another writer replaced it since.
|
|
114
|
+
*/
|
|
115
|
+
export function setImageBlobWriter(next: ImageBlobWriterSync): () => void {
|
|
116
|
+
hostBlobWriter = next;
|
|
117
|
+
blobLinks.clear();
|
|
118
|
+
return () => {
|
|
119
|
+
if (hostBlobWriter !== next) return;
|
|
120
|
+
hostBlobWriter = undefined;
|
|
121
|
+
blobLinks.clear();
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Whether transcript images carry links under the current `tui.hyperlinks` policy: a
|
|
127
|
+
* native surface honours only an explicit `off`, text mode the resolved OSC 8 support.
|
|
128
|
+
* Renderers caching a linked image key their cache on it.
|
|
129
|
+
*/
|
|
130
|
+
export function imageLinksEnabled(): boolean {
|
|
131
|
+
return isNativeRendering() ? isNativeLinkEnabled() : isHyperlinkEnabled();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The file a transcript image opens: `sourcePath` when it was read from one, else a
|
|
136
|
+
* blob copy of its bytes written through the host. Undefined when links are disabled
|
|
137
|
+
* or no file exists, so a disabled policy never writes blobs.
|
|
138
|
+
*/
|
|
139
|
+
export function transcriptImageLink(
|
|
140
|
+
image: Pick<ImageContent, "data" | "mimeType">,
|
|
141
|
+
sourcePath?: string,
|
|
142
|
+
): string | undefined {
|
|
143
|
+
if (!imageLinksEnabled()) return undefined;
|
|
144
|
+
if (sourcePath) return sourcePath;
|
|
145
|
+
if (!hostBlobWriter || !image.data) return undefined;
|
|
146
|
+
const key = `${image.mimeType}:${Bun.hash(image.data)}`;
|
|
147
|
+
const cached = blobLinks.get(key);
|
|
148
|
+
if (cached) return cached;
|
|
149
|
+
const link = materializeImageReferenceLink({ type: "image", ...image }, 1, hostBlobWriter);
|
|
150
|
+
if (!link) return undefined;
|
|
151
|
+
if (blobLinks.size >= BLOB_LINK_CACHE_LIMIT) {
|
|
152
|
+
const oldest = blobLinks.keys().next();
|
|
153
|
+
if (!oldest.done) blobLinks.delete(oldest.value);
|
|
154
|
+
}
|
|
155
|
+
blobLinks.set(key, link);
|
|
156
|
+
return link;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** `text` (an image's placeholder) as an OSC 8 link to the image's file; plain when hyperlinks are off or no file exists. */
|
|
160
|
+
export function linkedImageText(
|
|
161
|
+
text: string,
|
|
162
|
+
image: Pick<ImageContent, "data" | "mimeType">,
|
|
163
|
+
sourcePath?: string,
|
|
164
|
+
): string {
|
|
165
|
+
if (!isHyperlinkEnabled()) return text;
|
|
166
|
+
const target = transcriptImageLink(image, sourcePath);
|
|
167
|
+
return target ? fileHyperlink(target, text) : text;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Native counterpart of {@link linkedImageText}: a span linking to the image's file when one exists. */
|
|
171
|
+
export function linkedImageSpan(
|
|
172
|
+
text: string,
|
|
173
|
+
image: Pick<ImageContent, "data" | "mimeType">,
|
|
174
|
+
sourcePath: string | undefined,
|
|
175
|
+
s?: string,
|
|
176
|
+
): TspSpan {
|
|
177
|
+
const target = transcriptImageLink(image, sourcePath);
|
|
178
|
+
return target ? fileLinkSpan(target, text, s) : span(text, s);
|
|
179
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as native from "@linxiraos/pi-natives";
|
|
2
2
|
import { TERMINAL } from "../index";
|
|
3
3
|
import type { EditorInlineReplacement, EditorTextAssistProvider, EditorWordReplacements } from "../components/editor";
|
|
4
|
-
import { logger } from "@linxiraos/pi-utils";
|
|
4
|
+
import { logger, withTimeout } from "@linxiraos/pi-utils";
|
|
5
5
|
import { isMagicKeyword } from "./magic-keywords";
|
|
6
6
|
import { maskNonProse } from "./markdown-prose";
|
|
7
7
|
import { isProseWord, lineContext, ProseSource, type SpellingDecorationContext } from "./prose-gate";
|
|
@@ -45,6 +45,12 @@ const NATIVE_BACKEND: SpellingBackend = {
|
|
|
45
45
|
spellingGuesses: (text, start, length) => native.macOSSpellingGuesses(text, start, length),
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* The native side awaits a `flume::bounded(1)` reply with no timeout, so a stuck call
|
|
50
|
+
* stays pending and never reaches the `catch` that disables the provider.
|
|
51
|
+
*/
|
|
52
|
+
const BACKEND_TIMEOUT_MS = 2_000;
|
|
53
|
+
|
|
48
54
|
/**
|
|
49
55
|
* Bridges Apple's spelling service into the editor's typo and autocorrection
|
|
50
56
|
* paths. Word completion is the cross-platform `WordCompletionProvider`
|
|
@@ -113,7 +119,8 @@ export class MacOSSpellingProvider implements EditorTextAssistProvider {
|
|
|
113
119
|
|
|
114
120
|
/**
|
|
115
121
|
* Misspelled prose ranges in `text` (offsets into it), in order and
|
|
116
|
-
* non-overlapping
|
|
122
|
+
* non-overlapping, except ones touching `context.typing` (the word still
|
|
123
|
+
* being typed). Schedules the verification check for uncached text; its
|
|
117
124
|
* result arrives through {@link onUpdate}.
|
|
118
125
|
*/
|
|
119
126
|
typoRanges(text: string, context: SpellingDecorationContext): readonly native.SpellingRange[] {
|
|
@@ -131,9 +138,13 @@ export class MacOSSpellingProvider implements EditorTextAssistProvider {
|
|
|
131
138
|
const ranges = cached ?? this.#projectTypoRanges(text);
|
|
132
139
|
if (!ranges || ranges.length === 0) return [];
|
|
133
140
|
const kept: native.SpellingRange[] = [];
|
|
141
|
+
const typing = context.typing?.line === context.line ? context.typing : undefined;
|
|
134
142
|
let cursor = 0;
|
|
135
143
|
for (const range of ranges) {
|
|
136
144
|
const end = range.start + range.length;
|
|
145
|
+
if (typing && context.startCol + range.start <= typing.end && typing.start <= context.startCol + end) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
137
148
|
// Overlapping or out-of-bounds ranges (stale native data, edit projection)
|
|
138
149
|
// must never re-emit already-rendered text: that doubles it on screen and
|
|
139
150
|
// desyncs the rendered width from the measured width (cursor drift).
|
|
@@ -167,7 +178,11 @@ export class MacOSSpellingProvider implements EditorTextAssistProvider {
|
|
|
167
178
|
const context = lineContext(lines, cursorLine);
|
|
168
179
|
if (!this.#prose.isProse(context, start, start + word.length)) return null;
|
|
169
180
|
try {
|
|
170
|
-
const correction = await
|
|
181
|
+
const correction = await withTimeout(
|
|
182
|
+
this.backend.autocorrectWord(textBeforeCursor, start, word.length),
|
|
183
|
+
BACKEND_TIMEOUT_MS,
|
|
184
|
+
`macOS spelling service did not respond within ${BACKEND_TIMEOUT_MS}ms`,
|
|
185
|
+
);
|
|
171
186
|
if (!correction || correction === word) return null;
|
|
172
187
|
return { replaceLen: word.length + boundary.length, insert: correction + boundary };
|
|
173
188
|
} catch (error) {
|
|
@@ -200,7 +215,11 @@ export class MacOSSpellingProvider implements EditorTextAssistProvider {
|
|
|
200
215
|
try {
|
|
201
216
|
const seen = new Set<string>();
|
|
202
217
|
const items: string[] = [];
|
|
203
|
-
for (const guess of await
|
|
218
|
+
for (const guess of await withTimeout(
|
|
219
|
+
this.backend.spellingGuesses(line, range.start, range.length),
|
|
220
|
+
BACKEND_TIMEOUT_MS,
|
|
221
|
+
`macOS spelling service did not respond within ${BACKEND_TIMEOUT_MS}ms`,
|
|
222
|
+
)) {
|
|
204
223
|
if (!guess || seen.has(guess)) continue;
|
|
205
224
|
seen.add(guess);
|
|
206
225
|
items.push(guess);
|
|
@@ -305,7 +324,11 @@ export class MacOSSpellingProvider implements EditorTextAssistProvider {
|
|
|
305
324
|
async #fetchTypoRanges(text: string, generation: number): Promise<readonly native.SpellingRange[]> {
|
|
306
325
|
let checked: readonly native.SpellingRange[];
|
|
307
326
|
try {
|
|
308
|
-
checked = await
|
|
327
|
+
checked = await withTimeout(
|
|
328
|
+
this.backend.checkSpelling(text),
|
|
329
|
+
BACKEND_TIMEOUT_MS,
|
|
330
|
+
`macOS spelling service did not respond within ${BACKEND_TIMEOUT_MS}ms`,
|
|
331
|
+
);
|
|
309
332
|
} catch (error) {
|
|
310
333
|
this.#disable(error);
|
|
311
334
|
return [];
|
package/src/prompt/prose-gate.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { maskNonProse } from "./markdown-prose";
|
|
2
|
+
import type { TypingWord } from "./typing-word";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Client-side gates shared by the prose assistants (macOS typo/autocorrect and
|
|
@@ -19,6 +20,8 @@ export interface SpellingDecorationContext {
|
|
|
19
20
|
lines: readonly string[];
|
|
20
21
|
line: number;
|
|
21
22
|
startCol: number;
|
|
23
|
+
/** The word being typed: typo marks touching it wait until the caret leaves it. */
|
|
24
|
+
typing?: TypingWord | undefined;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
function tokenAt(text: string, start: number, end: number): string {
|