@linxiraos/pi-tui 1.1.27 → 1.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 +50 -461
- package/README.md +7 -5
- package/dist/types/app-keybindings.d.ts +1 -1
- package/dist/types/apps/cleanse-board.d.ts +1 -1
- package/dist/types/apps/debug/log-viewer.d.ts +4 -0
- package/dist/types/apps/debug/raw-sse-buffer.d.ts +2 -0
- package/dist/types/apps/git/git-tui.d.ts +2 -2
- package/dist/types/apps/git/help.d.ts +3 -1
- package/dist/types/apps/if-bench-board.d.ts +2 -2
- package/dist/types/apps/setup-model-picker.d.ts +1 -1
- package/dist/types/autocomplete.d.ts +5 -2
- package/dist/types/charts/chart-plan.d.ts +674 -0
- package/dist/types/charts/chart-svg.d.ts +59 -0
- package/dist/types/charts/table-data.d.ts +175 -0
- package/dist/types/chat/assistant-message.d.ts +5 -0
- package/dist/types/chat/chat-transcript-builder.d.ts +2 -0
- package/dist/types/chat/compaction-summary-message.d.ts +1 -0
- package/dist/types/chat/execution-shared.d.ts +6 -5
- package/dist/types/chat/fence-figure.d.ts +42 -0
- package/dist/types/chat/figure-markdown.d.ts +42 -0
- package/dist/types/chat/messages.d.ts +1 -1
- package/dist/types/chat/svg-figure.d.ts +33 -0
- package/dist/types/chat/svg-source.d.ts +49 -0
- package/dist/types/chat/table-chart.d.ts +89 -0
- package/dist/types/chat/tool-execution.d.ts +2 -0
- package/dist/types/chat/user-message.d.ts +15 -2
- package/dist/types/chrome/shared.d.ts +9 -0
- package/dist/types/chrome/transcript-container.d.ts +6 -0
- package/dist/types/components/box.d.ts +1 -0
- package/dist/types/components/disclosure.d.ts +1 -0
- package/dist/types/components/editor.d.ts +15 -4
- package/dist/types/components/form.d.ts +1 -0
- package/dist/types/components/image.d.ts +12 -44
- package/dist/types/components/key-value-list.d.ts +1 -0
- package/dist/types/components/layout/row.d.ts +1 -0
- package/dist/types/components/layout/stack.d.ts +1 -0
- package/dist/types/components/markdown.d.ts +36 -3
- package/dist/types/components/scroll-view.d.ts +1 -0
- package/dist/types/components/section.d.ts +1 -0
- package/dist/types/components/table.d.ts +1 -0
- package/dist/types/components/text.d.ts +1 -0
- package/dist/types/components/tree-view.d.ts +7 -0
- package/dist/types/components/truncated-text.d.ts +1 -0
- package/dist/types/fuzzy.d.ts +29 -1
- package/dist/types/key-hint-format.d.ts +1 -1
- package/dist/types/keybinding-matchers.d.ts +3 -0
- package/dist/types/keybindings.d.ts +1 -1
- package/dist/types/kitty-graphics.d.ts +2 -2
- package/dist/types/loop-watchdog.d.ts +8 -7
- package/dist/types/multiplexers/cmux.d.ts +16 -0
- package/dist/types/multiplexers/herdr.d.ts +26 -0
- package/dist/types/multiplexers/notify.d.ts +13 -0
- package/dist/types/multiplexers/orca.d.ts +12 -0
- package/dist/types/multiplexers/screen.d.ts +9 -0
- package/dist/types/multiplexers/tmux.d.ts +29 -0
- package/dist/types/multiplexers/types.d.ts +100 -0
- package/dist/types/multiplexers/wmux.d.ts +12 -0
- package/dist/types/multiplexers/zellij.d.ts +12 -0
- package/dist/types/native/backend.d.ts +3 -2
- package/dist/types/native/blobs.d.ts +7 -3
- package/dist/types/native/encode.d.ts +3 -4
- package/dist/types/native/overlay.d.ts +5 -0
- package/dist/types/overlays/agent-hub-projection.d.ts +6 -5
- package/dist/types/overlays/agent-hub-types.d.ts +9 -0
- package/dist/types/overlays/agent-hub.d.ts +2 -0
- package/dist/types/overlays/agent-transcript-viewer.d.ts +11 -2
- package/dist/types/overlays/agents-hub.d.ts +9 -0
- package/dist/types/overlays/annotation-overlay.d.ts +19 -2
- package/dist/types/overlays/annotation-types.d.ts +2 -0
- package/dist/types/overlays/ask-dialog.d.ts +5 -2
- package/dist/types/overlays/history-search.d.ts +10 -0
- package/dist/types/overlays/hook-selector.d.ts +9 -4
- package/dist/types/overlays/jobs-panel.d.ts +1 -7
- package/dist/types/overlays/model-browser.d.ts +54 -7
- package/dist/types/overlays/model-hub.d.ts +23 -0
- package/dist/types/overlays/oauth-selector.d.ts +0 -1
- package/dist/types/overlays/plan-review-overlay.d.ts +3 -2
- package/dist/types/overlays/session-observer-registry.d.ts +2 -0
- package/dist/types/overlays/session-selector.d.ts +6 -2
- package/dist/types/overlays/settings-defs.d.ts +1 -0
- package/dist/types/overlays/usage-dashboard.d.ts +8 -4
- package/dist/types/overlays/usage-display.d.ts +3 -1
- package/dist/types/prompt/composer.d.ts +2 -7
- package/dist/types/prompt/custom-editor.d.ts +11 -8
- package/dist/types/prompt/image-source.d.ts +2 -1
- package/dist/types/prompt/interactive-context-helpers.d.ts +4 -1
- package/dist/types/prompt/model-mention-autocomplete.d.ts +1 -1
- package/dist/types/prompt/welcome.d.ts +19 -42
- package/dist/types/render/composite.d.ts +2 -0
- package/dist/types/render/output-pane.d.ts +1 -0
- package/dist/types/render/render-utils.d.ts +1 -0
- package/dist/types/render/tool-card.d.ts +6 -0
- package/dist/types/render/width-aware-text.d.ts +2 -0
- package/dist/types/setup/scenes/splash.d.ts +1 -1
- package/dist/types/setup/scenes/types.d.ts +1 -0
- package/dist/types/status-line/component.d.ts +3 -0
- package/dist/types/status-line/host.d.ts +4 -1
- package/dist/types/status-line/segments.d.ts +2 -0
- package/dist/types/status-line/types.d.ts +14 -3
- package/dist/types/terminal-capabilities.d.ts +27 -35
- package/dist/types/terminal-multiplexer.d.ts +127 -14
- package/dist/types/terminal.d.ts +22 -6
- package/dist/types/theme/active-symbols.d.ts +1 -1
- package/dist/types/theme/schema.d.ts +11 -2
- package/dist/types/theme/symbols.d.ts +1 -1
- package/dist/types/theme/theme-class.d.ts +7 -0
- package/dist/types/theme/theme.d.ts +1 -1
- package/dist/types/theme/tui-adapters.d.ts +3 -0
- package/dist/types/tmux.d.ts +11 -4
- package/dist/types/tools/daemon.d.ts +1 -1
- package/dist/types/tools/eval.d.ts +22 -3
- package/dist/types/tools/glob.d.ts +4 -0
- package/dist/types/tools/native-view.d.ts +47 -0
- package/dist/types/tools/output-meta.d.ts +11 -1
- package/dist/types/tools/renderer.d.ts +37 -2
- package/dist/types/tools/streaming-output.d.ts +20 -1
- package/dist/types/tools/task.d.ts +0 -4
- package/dist/types/tools/write.d.ts +9 -2
- package/dist/types/tools/yield.d.ts +22 -0
- package/dist/types/tui.d.ts +31 -0
- package/dist/types/utils.d.ts +6 -0
- package/dist/types/vim.d.ts +19 -5
- package/package.json +13 -9
- package/src/app-keybindings.d.ts +1 -1
- package/src/app-keybindings.ts +1 -1
- package/src/apps/autoresearch-dashboard.ts +7 -3
- package/src/apps/cleanse-board.ts +2 -2
- package/src/apps/cleanse-picker.ts +1 -1
- package/src/apps/debug/log-viewer.ts +188 -72
- package/src/apps/debug/raw-sse-buffer.ts +8 -0
- package/src/apps/debug/raw-sse.ts +39 -5
- package/src/apps/git/diff-pane.ts +26 -5
- package/src/apps/git/git-tui.ts +2 -2
- package/src/apps/git/help.ts +29 -20
- package/src/apps/if-bench-board.ts +2 -2
- package/src/apps/ps-top.ts +2 -2
- package/src/apps/setup-model-picker.ts +1 -1
- package/src/autocomplete.d.ts +5 -2
- package/src/autocomplete.ts +67 -43
- package/src/charts/chart-plan.d.ts +674 -0
- package/src/charts/chart-plan.ts +5081 -0
- package/src/charts/chart-svg.d.ts +59 -0
- package/src/charts/chart-svg.ts +4657 -0
- package/src/charts/table-data.d.ts +175 -0
- package/src/charts/table-data.ts +1478 -0
- package/src/chat/advisor-message.ts +11 -0
- package/src/chat/assistant-message.d.ts +5 -0
- package/src/chat/assistant-message.ts +219 -50
- package/src/chat/chat-transcript-builder.d.ts +2 -0
- package/src/chat/chat-transcript-builder.ts +5 -3
- package/src/chat/compaction-summary-message.d.ts +1 -0
- package/src/chat/compaction-summary-message.ts +8 -0
- package/src/chat/execution-shared.d.ts +6 -5
- package/src/chat/execution-shared.ts +42 -25
- package/src/chat/fence-figure.d.ts +42 -0
- package/src/chat/fence-figure.ts +80 -0
- package/src/chat/figure-markdown.d.ts +42 -0
- package/src/chat/figure-markdown.ts +174 -0
- package/src/chat/messages.d.ts +1 -1
- package/src/chat/messages.ts +1 -1
- package/src/chat/read-tool-group.ts +6 -4
- package/src/chat/skill-message.ts +6 -0
- package/src/chat/svg-figure.d.ts +33 -0
- package/src/chat/svg-figure.ts +399 -0
- package/src/chat/svg-source.d.ts +49 -0
- package/src/chat/svg-source.ts +188 -0
- package/src/chat/table-chart.d.ts +89 -0
- package/src/chat/table-chart.ts +294 -0
- package/src/chat/tool-execution.d.ts +2 -0
- package/src/chat/tool-execution.ts +80 -6
- package/src/chat/user-message.d.ts +15 -2
- package/src/chat/user-message.ts +55 -3
- package/src/chrome/shared.ts +53 -0
- package/src/chrome/transcript-container.d.ts +6 -0
- package/src/chrome/transcript-container.ts +93 -13
- package/src/components/box.d.ts +1 -0
- package/src/components/box.ts +5 -0
- package/src/components/disclosure.d.ts +1 -0
- package/src/components/disclosure.ts +8 -0
- package/src/components/editor.d.ts +15 -4
- package/src/components/editor.ts +294 -46
- package/src/components/form.d.ts +1 -0
- package/src/components/form.ts +8 -0
- package/src/components/image.d.ts +12 -44
- package/src/components/image.ts +47 -151
- package/src/components/key-value-list.d.ts +1 -0
- package/src/components/key-value-list.ts +4 -0
- package/src/components/layout/row.d.ts +1 -0
- package/src/components/layout/row.ts +7 -0
- package/src/components/layout/stack.d.ts +1 -0
- package/src/components/layout/stack.ts +7 -0
- package/src/components/markdown.d.ts +36 -3
- package/src/components/markdown.ts +433 -318
- package/src/components/scroll-view.d.ts +1 -0
- package/src/components/scroll-view.ts +5 -0
- package/src/components/section.d.ts +1 -0
- package/src/components/section.ts +5 -0
- package/src/components/settings-list.ts +2 -0
- package/src/components/table.d.ts +1 -0
- package/src/components/table.ts +4 -0
- package/src/components/text.d.ts +1 -0
- package/src/components/text.ts +7 -0
- package/src/components/tree-view.d.ts +7 -0
- package/src/components/tree-view.ts +38 -8
- package/src/components/truncated-text.d.ts +1 -0
- package/src/components/truncated-text.ts +5 -0
- package/src/desktop-notify.ts +1 -1
- package/src/fuzzy.d.ts +29 -1
- package/src/fuzzy.ts +115 -30
- package/src/key-hint-format.d.ts +1 -1
- package/src/key-hint-format.ts +1 -1
- package/src/keybinding-matchers.d.ts +3 -0
- package/src/keybinding-matchers.ts +6 -2
- package/src/keybindings.d.ts +1 -1
- package/src/keybindings.ts +1 -1
- package/src/kitty-graphics.d.ts +2 -2
- package/src/kitty-graphics.ts +4 -4
- package/src/loop-watchdog.d.ts +8 -7
- package/src/loop-watchdog.ts +18 -13
- package/src/multiplexers/cmux.d.ts +16 -0
- package/src/multiplexers/cmux.ts +29 -0
- package/src/multiplexers/herdr.d.ts +26 -0
- package/src/multiplexers/herdr.ts +64 -0
- package/src/multiplexers/notify.d.ts +13 -0
- package/src/multiplexers/notify.ts +24 -0
- package/src/multiplexers/orca.d.ts +12 -0
- package/src/multiplexers/orca.ts +16 -0
- package/src/multiplexers/screen.d.ts +9 -0
- package/src/multiplexers/screen.ts +15 -0
- package/src/multiplexers/tmux.d.ts +29 -0
- package/src/multiplexers/tmux.ts +40 -0
- package/src/multiplexers/types.d.ts +100 -0
- package/src/multiplexers/types.ts +110 -0
- package/src/multiplexers/wmux.d.ts +12 -0
- package/src/multiplexers/wmux.ts +16 -0
- package/src/multiplexers/zellij.d.ts +12 -0
- package/src/multiplexers/zellij.ts +24 -0
- package/src/native/backend.d.ts +3 -2
- package/src/native/backend.ts +174 -18
- package/src/native/blobs.d.ts +7 -3
- package/src/native/blobs.ts +47 -12
- package/src/native/encode.d.ts +3 -4
- package/src/native/encode.ts +58 -31
- package/src/native/overlay.d.ts +5 -0
- package/src/native/overlay.ts +13 -0
- package/src/native/reconcile.ts +4 -0
- package/src/overlays/agent-hub-projection.d.ts +6 -5
- package/src/overlays/agent-hub-projection.ts +124 -13
- package/src/overlays/agent-hub-types.d.ts +9 -0
- package/src/overlays/agent-hub-types.ts +6 -0
- package/src/overlays/agent-hub.ts +46 -31
- package/src/overlays/agent-transcript-viewer.ts +91 -23
- package/src/overlays/agents-hub.ts +28 -8
- package/src/overlays/annotation-overlay.ts +293 -90
- package/src/overlays/annotation-types.ts +2 -0
- package/src/overlays/ask-dialog.ts +7 -10
- package/src/overlays/btw-history-panel.ts +12 -1
- package/src/overlays/copy-selector.ts +30 -8
- package/src/overlays/extensions/state-manager.ts +52 -12
- package/src/overlays/history-search.ts +100 -7
- package/src/overlays/hook-selector.ts +12 -5
- package/src/overlays/jobs-panel.ts +28 -46
- package/src/overlays/model-browser.ts +341 -110
- package/src/overlays/model-hub.ts +300 -49
- package/src/overlays/move-overlay.ts +28 -14
- package/src/overlays/oauth-selector.ts +54 -70
- package/src/overlays/plan-review-overlay.ts +194 -70
- package/src/overlays/plugin-settings.ts +11 -5
- package/src/overlays/report-panel.ts +2 -2
- package/src/overlays/rewind-selector.ts +86 -17
- package/src/overlays/session-observer-registry.d.ts +2 -0
- package/src/overlays/session-observer-registry.ts +10 -2
- package/src/overlays/session-selector.ts +64 -26
- package/src/overlays/settings-defs.ts +4 -3
- package/src/overlays/settings-selector.ts +33 -8
- package/src/overlays/snapcompact-shape-preview.ts +2 -3
- package/src/overlays/stats-notice.ts +1 -1
- package/src/overlays/tree-selector.ts +41 -20
- package/src/overlays/usage-dashboard.ts +100 -50
- package/src/overlays/usage-display.ts +10 -4
- package/src/prompt/attachment-chips.ts +15 -11
- package/src/prompt/composer-attachments.ts +6 -4
- package/src/prompt/composer-cache.ts +4 -6
- package/src/prompt/composer.ts +6 -37
- package/src/prompt/custom-editor.ts +54 -26
- package/src/prompt/image-source.ts +2 -1
- package/src/prompt/interactive-context-helpers.ts +30 -18
- package/src/prompt/macos-spelling.ts +7 -0
- package/src/prompt/magic-keywords.ts +11 -2
- package/src/prompt/model-mention-autocomplete.ts +14 -14
- package/src/prompt/tips.txt +6 -5
- package/src/prompt/welcome.ts +235 -411
- package/src/prompt/word-completion.ts +38 -5
- package/src/render/composite.d.ts +2 -0
- package/src/render/composite.ts +64 -0
- package/src/render/output-pane.d.ts +1 -0
- package/src/render/output-pane.ts +6 -0
- package/src/render/render-utils.d.ts +1 -0
- package/src/render/render-utils.ts +11 -0
- package/src/render/tool-card.d.ts +6 -0
- package/src/render/tool-card.ts +15 -0
- package/src/render/width-aware-text.d.ts +2 -0
- package/src/render/width-aware-text.ts +5 -0
- package/src/setup/scenes/model.ts +2 -1
- package/src/setup/scenes/splash.ts +116 -53
- package/src/setup/scenes/types.ts +1 -0
- package/src/status-line/component.ts +292 -78
- package/src/status-line/host.ts +4 -1
- package/src/status-line/segments.ts +40 -22
- package/src/status-line/types.ts +15 -3
- package/src/terminal-capabilities.d.ts +27 -35
- package/src/terminal-capabilities.ts +75 -189
- package/src/terminal-multiplexer.d.ts +127 -14
- package/src/terminal-multiplexer.ts +112 -33
- package/src/terminal.d.ts +22 -6
- package/src/terminal.ts +139 -37
- package/src/theme/active-symbols.d.ts +1 -1
- package/src/theme/active-symbols.ts +1 -1
- package/src/theme/schema-validation.ts +1 -0
- package/src/theme/schema.d.ts +11 -2
- package/src/theme/schema.ts +14 -1
- package/src/theme/session-color.ts +1 -7
- package/src/theme/symbols.d.ts +1 -1
- package/src/theme/symbols.ts +6 -1
- package/src/theme/theme-class.d.ts +7 -0
- package/src/theme/theme-class.ts +26 -1
- package/src/theme/theme-schema.json +5 -1
- package/src/theme/theme.d.ts +1 -1
- package/src/theme/theme.ts +8 -4
- package/src/theme/tui-adapters.d.ts +3 -0
- package/src/theme/tui-adapters.ts +68 -0
- package/src/tmux.d.ts +11 -4
- package/src/tmux.ts +47 -11
- package/src/tools/ast-edit.ts +59 -35
- package/src/tools/ast-grep.ts +35 -16
- package/src/tools/bash.ts +92 -42
- package/src/tools/daemon.d.ts +1 -1
- package/src/tools/daemon.ts +1 -1
- package/src/tools/default-renderer.ts +35 -19
- package/src/tools/edit.ts +142 -120
- package/src/tools/eval-format/index.ts +28 -2
- package/src/tools/eval-format/javascript.ts +7 -5
- package/src/tools/eval.d.ts +22 -3
- package/src/tools/eval.ts +364 -208
- package/src/tools/fetch.ts +5 -6
- package/src/tools/glob.d.ts +4 -0
- package/src/tools/glob.ts +33 -65
- package/src/tools/grep.ts +81 -48
- package/src/tools/index.ts +2 -0
- package/src/tools/lsp.ts +81 -59
- package/src/tools/mcp.ts +111 -68
- package/src/tools/native-view.d.ts +47 -0
- package/src/tools/native-view.ts +102 -1
- package/src/tools/output-meta.d.ts +11 -1
- package/src/tools/output-meta.ts +23 -3
- package/src/tools/read.ts +7 -5
- package/src/tools/renderer.d.ts +37 -2
- package/src/tools/renderer.ts +42 -2
- package/src/tools/streaming-output.d.ts +20 -1
- package/src/tools/streaming-output.ts +149 -50
- package/src/tools/task.d.ts +0 -4
- package/src/tools/task.ts +112 -59
- package/src/tools/todo.ts +119 -30
- package/src/tools/wait.ts +34 -24
- package/src/tools/web-search.ts +1 -1
- package/src/tools/write.d.ts +9 -2
- package/src/tools/write.ts +147 -95
- package/src/tools/yield.d.ts +22 -0
- package/src/tools/yield.ts +225 -0
- package/src/tui.d.ts +31 -0
- package/src/tui.ts +309 -160
- package/src/utils.d.ts +6 -0
- package/src/utils.ts +19 -0
- package/src/vim.d.ts +19 -5
- package/src/vim.ts +333 -18
- package/src/windows-input-mode.ts +8 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { TerminalMultiplexerModule } from "./types";
|
|
2
|
+
|
|
3
|
+
export const zellijMultiplexer = {
|
|
4
|
+
id: "zellij",
|
|
5
|
+
precedence: "session",
|
|
6
|
+
sessionEnvKeys: ["ZELLIJ"],
|
|
7
|
+
ownsScreenGrid: true,
|
|
8
|
+
isInside(env: NodeJS.ProcessEnv = Bun.env): boolean {
|
|
9
|
+
return Boolean(env.ZELLIJ);
|
|
10
|
+
},
|
|
11
|
+
notifier: {
|
|
12
|
+
tier: "inBand",
|
|
13
|
+
// Zellij drops OSCs and has no DCS passthrough; a bare BEL raises its bell
|
|
14
|
+
// flag. Bell notifications already provide that signal.
|
|
15
|
+
send({ sequence, write }) {
|
|
16
|
+
if (sequence === null) return false;
|
|
17
|
+
write(`${sequence}\x07`);
|
|
18
|
+
return true;
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
// Zellij currently breaks OSC 11 passthrough on macOS, so terminal-derived
|
|
22
|
+
// appearance cannot be trusted there.
|
|
23
|
+
backgroundQueryBrokenOn: ["darwin"],
|
|
24
|
+
} as const satisfies TerminalMultiplexerModule;
|
package/src/native/backend.d.ts
CHANGED
|
@@ -48,8 +48,9 @@ export interface NativeBackendOptions {
|
|
|
48
48
|
/**
|
|
49
49
|
* The reply a v1 terminal is assumed to give before its real `hello` arrives
|
|
50
50
|
* (the `TERM_PROGRAM=tern` optimistic start): the whole vocabulary, the
|
|
51
|
-
* default APC limit and credits, the terminal's width, the appearance
|
|
52
|
-
* already detected, and
|
|
51
|
+
* default APC limit and credits, the terminal's width, the appearance omp
|
|
52
|
+
* already detected, full motion, and `blobs`, so a resumed session's images
|
|
53
|
+
* are asked about rather than resent before the reply comes.
|
|
53
54
|
*/
|
|
54
55
|
export declare function assumedTspHello(terminal: Terminal): TspHello;
|
|
55
56
|
export declare class NativeBackend {
|
package/src/native/backend.ts
CHANGED
|
@@ -14,8 +14,18 @@
|
|
|
14
14
|
* render while blocked only marks the surface dirty; the next `ack` reconciles
|
|
15
15
|
* once against the last sent state, so every intermediate change coalesces
|
|
16
16
|
* into one frame (`set`s merged, `text append`s joined).
|
|
17
|
+
*
|
|
18
|
+
* Blobs. Each image's bytes reach the terminal once per connection. A
|
|
19
|
+
* terminal with the `blobs` feature is asked first which it already holds:
|
|
20
|
+
* under `TERN_BLOB_DIR` (Tern's on-disk blob cache, on this machine) every new
|
|
21
|
+
* blob is written there and then asked about, so its bytes never cross the
|
|
22
|
+
* pty; otherwise the first pass after (re)connecting asks, so a resumed
|
|
23
|
+
* session doesn't resend what the terminal kept. Whatever the reply lacks
|
|
24
|
+
* (or a reply that never comes) is sent inline with `b`. Frames never wait:
|
|
25
|
+
* the terminal draws an image whose blob arrives after the frame.
|
|
17
26
|
*/
|
|
18
27
|
import * as fs from "node:fs";
|
|
28
|
+
import * as path from "node:path";
|
|
19
29
|
import * as logger from "@linxiraos/pi-utils/logger";
|
|
20
30
|
import {
|
|
21
31
|
TSP_DEFAULT_APC_LIMIT,
|
|
@@ -40,7 +50,7 @@ import {
|
|
|
40
50
|
} from "../theme/theme";
|
|
41
51
|
import type { Component, OverlayOptions, RenderScheduler, RenderTimer } from "../tui";
|
|
42
52
|
import { TspDocument } from "./apply";
|
|
43
|
-
import { getNativeBlob } from "./blobs";
|
|
53
|
+
import { getNativeBlob, type NativeBlob } from "./blobs";
|
|
44
54
|
import { node } from "./describe";
|
|
45
55
|
import { encodeTspJson, encodeTspMessage, type TspHello, TspReader, splitTspMessage } from "./encode";
|
|
46
56
|
import type { DescribeContext, NativeChild, NativeNode, NativeSurface, NativeUiEvent } from "./node";
|
|
@@ -115,6 +125,44 @@ const RECENT_FRAMES = 64;
|
|
|
115
125
|
const STALLED_ACK_MS = 5000;
|
|
116
126
|
/** Role of the session's surfaces; a screen page may name its own. */
|
|
117
127
|
const SESSION_ROLE = "zeta.session";
|
|
128
|
+
/** A `blobs` query unanswered this long counts its ids as missing: they go inline. */
|
|
129
|
+
const BLOB_REPLY_MS = 3000;
|
|
130
|
+
/** Expired queries kept to pair late replies with their queries, at most. */
|
|
131
|
+
const MAX_BLOB_QUERIES = 16;
|
|
132
|
+
|
|
133
|
+
/** A `blobs` query awaiting its reply; `timer` is unset once its ids no longer depend on it. */
|
|
134
|
+
interface BlobQuery {
|
|
135
|
+
readonly ids: readonly string[];
|
|
136
|
+
timer: RenderTimer | undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Save `blob` in the terminal's blob cache as `<dir>/<id>` (written aside,
|
|
141
|
+
* then renamed; skipped when present). A failure only means the terminal
|
|
142
|
+
* won't find it, so the blob goes inline.
|
|
143
|
+
*/
|
|
144
|
+
async function cacheBlob(dir: string, blob: NativeBlob): Promise<void> {
|
|
145
|
+
const target = path.join(dir, blob.id);
|
|
146
|
+
const temp = `${target}.tmp${process.pid}`;
|
|
147
|
+
try {
|
|
148
|
+
if (await Bun.file(target).exists()) return;
|
|
149
|
+
// Not `Bun.write`: it would create a missing folder, which only Tern may.
|
|
150
|
+
await fs.promises.writeFile(temp, blob.bytes);
|
|
151
|
+
await fs.promises.rename(temp, target);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
logger.debug("TSP: could not save a blob to the terminal's cache", { dir, id: blob.id, error: String(error) });
|
|
154
|
+
await fs.promises.rm(temp, { force: true }).catch(() => {});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* A `b` message's parameters. Tern names a blob by the sha256 of the bytes it
|
|
160
|
+
* receives and ignores `id`, but Tern 0.5.3 and earlier reject a blob without
|
|
161
|
+
* it, so it stays until those are gone.
|
|
162
|
+
*/
|
|
163
|
+
function blobParams(blob: NativeBlob): Record<string, string> {
|
|
164
|
+
return { id: blob.id, mime: blob.mime };
|
|
165
|
+
}
|
|
118
166
|
|
|
119
167
|
class NativeContext implements DescribeContext {
|
|
120
168
|
cols: number;
|
|
@@ -157,8 +205,9 @@ function sameSet(a: ReadonlySet<string>, b: ReadonlySet<string>): boolean {
|
|
|
157
205
|
/**
|
|
158
206
|
* The reply a v1 terminal is assumed to give before its real `hello` arrives
|
|
159
207
|
* (the `TERM_PROGRAM=tern` optimistic start): the whole vocabulary, the
|
|
160
|
-
* default APC limit and credits, the terminal's width, the appearance
|
|
161
|
-
* already detected, and
|
|
208
|
+
* default APC limit and credits, the terminal's width, the appearance omp
|
|
209
|
+
* already detected, full motion, and `blobs`, so a resumed session's images
|
|
210
|
+
* are asked about rather than resent before the reply comes.
|
|
162
211
|
*/
|
|
163
212
|
export function assumedTspHello(terminal: Terminal): TspHello {
|
|
164
213
|
const appearance = terminal.appearance;
|
|
@@ -167,6 +216,7 @@ export function assumedTspHello(terminal: Terminal): TspHello {
|
|
|
167
216
|
v: TSP_VERSION,
|
|
168
217
|
term: "tern",
|
|
169
218
|
kinds: TSP_KINDS,
|
|
219
|
+
features: ["blobs"],
|
|
170
220
|
apc: TSP_DEFAULT_APC_LIMIT,
|
|
171
221
|
credits: TSP_DEFAULT_CREDITS,
|
|
172
222
|
cols: terminal.columns,
|
|
@@ -186,7 +236,6 @@ class Surface {
|
|
|
186
236
|
acked = 0;
|
|
187
237
|
/** Send times of unacknowledged frames, oldest first. */
|
|
188
238
|
unacked: number[] = [];
|
|
189
|
-
uploaded = new Set<string>();
|
|
190
239
|
focus: string | null = null;
|
|
191
240
|
dirty = false;
|
|
192
241
|
|
|
@@ -256,6 +305,14 @@ export class NativeBackend {
|
|
|
256
305
|
#paletteKey: string | undefined;
|
|
257
306
|
/** Serialized palette last sent, to skip resends of an unchanged theme. */
|
|
258
307
|
#paletteSent: string | undefined;
|
|
308
|
+
/** Pending delivery owns bytes until sent or confirmed held; settled descriptions may already be released. */
|
|
309
|
+
#blobs = new Map<string, NativeBlob | "sent">();
|
|
310
|
+
/** `blobs` queries sent, oldest first: the terminal answers each once, in order. */
|
|
311
|
+
#blobQueries: BlobQuery[] = [];
|
|
312
|
+
/** The next pass with new blobs asks the terminal which it holds (a fresh connection). */
|
|
313
|
+
#askHeld = true;
|
|
314
|
+
/** Bumped when the connection resets or stops, so blob cache writes finishing later are dropped. */
|
|
315
|
+
#blobEpoch = 0;
|
|
259
316
|
|
|
260
317
|
constructor(host: NativeHost, hello: TspHello, options: NativeBackendOptions = {}) {
|
|
261
318
|
this.#host = host;
|
|
@@ -369,6 +426,9 @@ export class NativeBackend {
|
|
|
369
426
|
this.#unbindTheme = undefined;
|
|
370
427
|
this.#useNerdSymbols(false);
|
|
371
428
|
this.#clearStallTimer();
|
|
429
|
+
// Replies no longer reach the backend: forget the queries outright.
|
|
430
|
+
this.#resetBlobs();
|
|
431
|
+
this.#blobQueries = [];
|
|
372
432
|
}
|
|
373
433
|
|
|
374
434
|
/**
|
|
@@ -383,13 +443,14 @@ export class NativeBackend {
|
|
|
383
443
|
this.#live = true;
|
|
384
444
|
this.#useNerdSymbols(true);
|
|
385
445
|
this.#watchTheme();
|
|
446
|
+
this.#resetBlobs();
|
|
386
447
|
const surface = this.#inline;
|
|
387
448
|
surface.reconciler.detachLive();
|
|
388
449
|
surface.unacked = [];
|
|
389
450
|
surface.acked = surface.seq;
|
|
390
451
|
surface.focus = null;
|
|
391
452
|
surface.dirty = false;
|
|
392
|
-
this.#write("o", { id: surface.id, mode: "inline", title: "
|
|
453
|
+
this.#write("o", { id: surface.id, mode: "inline", title: "omp", role: SESSION_ROLE, adopt: true });
|
|
393
454
|
this.#sendPalette(surface);
|
|
394
455
|
// After the `o`, as in `start()`.
|
|
395
456
|
setNativeRendering(true);
|
|
@@ -491,7 +552,7 @@ export class NativeBackend {
|
|
|
491
552
|
logger.debug("TSP frame", { sf: surface.id, ops: ops.length, rows: surface.reconciler.fallbackCount });
|
|
492
553
|
}
|
|
493
554
|
if (ops.length === 0) return;
|
|
494
|
-
this.#uploadBlobs(
|
|
555
|
+
this.#uploadBlobs(ops);
|
|
495
556
|
this.#sendFrame(surface, ops);
|
|
496
557
|
}
|
|
497
558
|
|
|
@@ -505,6 +566,7 @@ export class NativeBackend {
|
|
|
505
566
|
this.#record("in", raw.verb, raw.params, raw.body);
|
|
506
567
|
const message = this.#reader.feed(sequence);
|
|
507
568
|
if (message?.verb === "e") this.#handleEvent(message.event);
|
|
569
|
+
else if (message?.reply.r === "blobs") this.#onBlobsReply(message.reply.have);
|
|
508
570
|
return true;
|
|
509
571
|
}
|
|
510
572
|
|
|
@@ -530,7 +592,7 @@ export class NativeBackend {
|
|
|
530
592
|
}
|
|
531
593
|
|
|
532
594
|
#open(surface: Surface): void {
|
|
533
|
-
this.#write("o", { id: surface.id, mode: surface.mode, title: "
|
|
595
|
+
this.#write("o", { id: surface.id, mode: surface.mode, title: "omp", role: surface.role });
|
|
534
596
|
this.#sendPalette(surface);
|
|
535
597
|
}
|
|
536
598
|
|
|
@@ -583,25 +645,118 @@ export class NativeBackend {
|
|
|
583
645
|
this.#write("f", frame);
|
|
584
646
|
}
|
|
585
647
|
|
|
586
|
-
|
|
648
|
+
/** Deliver the blobs `ops` reference that this connection hasn't handled yet (see the module doc). */
|
|
649
|
+
#uploadBlobs(ops: readonly TspOp[]): void {
|
|
587
650
|
const ids = new Set<string>();
|
|
588
651
|
for (const op of ops) collectBlobs(op, ids);
|
|
652
|
+
const fresh: NativeBlob[] = [];
|
|
589
653
|
for (const id of ids) {
|
|
590
|
-
if (
|
|
654
|
+
if (this.#blobs.has(id)) continue;
|
|
591
655
|
const blob = getNativeBlob(id);
|
|
592
|
-
if (
|
|
593
|
-
|
|
594
|
-
|
|
656
|
+
if (blob) fresh.push(blob);
|
|
657
|
+
else logger.warn("TSP: image references an unregistered blob", { id });
|
|
658
|
+
}
|
|
659
|
+
if (fresh.length === 0) return;
|
|
660
|
+
if (!this.#cx.feature("blobs")) {
|
|
661
|
+
for (const blob of fresh) this.#sendBlob(blob);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const dir = Bun.env.TERN_BLOB_DIR;
|
|
665
|
+
if (dir) {
|
|
666
|
+
for (const blob of fresh) this.#blobs.set(blob.id, blob);
|
|
667
|
+
void this.#cacheBlobs(dir, fresh);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (this.#askHeld) {
|
|
671
|
+
this.#askHeld = false;
|
|
672
|
+
this.#askBlobs(fresh);
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
for (const blob of fresh) this.#sendBlob(blob);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/** Write `blobs` to the terminal's cache, then ask about the ones still pending. */
|
|
679
|
+
async #cacheBlobs(dir: string, blobs: readonly NativeBlob[]): Promise<void> {
|
|
680
|
+
const epoch = this.#blobEpoch;
|
|
681
|
+
await Promise.all(blobs.map(blob => cacheBlob(dir, blob)));
|
|
682
|
+
if (epoch !== this.#blobEpoch) return;
|
|
683
|
+
const pending = blobs.filter(blob => this.#blobs.get(blob.id) === blob);
|
|
684
|
+
if (pending.length > 0) this.#askBlobs(pending);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/** Ask the terminal which of `blobs` it holds; the reply (or its absence) settles them. */
|
|
688
|
+
#askBlobs(blobs: readonly NativeBlob[]): void {
|
|
689
|
+
for (const blob of blobs) this.#blobs.set(blob.id, blob);
|
|
690
|
+
const ids = blobs.map(blob => blob.id);
|
|
691
|
+
const query: BlobQuery = { ids, timer: undefined };
|
|
692
|
+
query.timer = this.#scheduler.scheduleRender(() => {
|
|
693
|
+
query.timer = undefined;
|
|
694
|
+
logger.warn("TSP: no reply to a blobs query; sending its blobs inline", { ids: ids.length });
|
|
695
|
+
this.#sendMissing(ids);
|
|
696
|
+
}, BLOB_REPLY_MS);
|
|
697
|
+
// Expired queries stay only to pair late replies; a terminal that never answers mustn't grow the list.
|
|
698
|
+
while (this.#blobQueries.length >= MAX_BLOB_QUERIES && !this.#blobQueries[0]!.timer) this.#blobQueries.shift();
|
|
699
|
+
this.#blobQueries.push(query);
|
|
700
|
+
this.#write("q", { q: "blobs", ids });
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* The reply to the oldest outstanding query. Any `have` id still asked
|
|
705
|
+
* about is held by the terminal (whichever query asked); the query's other
|
|
706
|
+
* ids go inline.
|
|
707
|
+
*/
|
|
708
|
+
#onBlobsReply(have: readonly string[]): void {
|
|
709
|
+
const query = this.#blobQueries.shift();
|
|
710
|
+
for (const id of have) {
|
|
711
|
+
const blob = this.#blobs.get(id);
|
|
712
|
+
if (blob === undefined || blob === "sent") continue;
|
|
713
|
+
// The bytes went another way; a recording still carries them so a replay without them shows the image.
|
|
714
|
+
if (this.#recordPath) {
|
|
715
|
+
const body = Buffer.from(blob.bytes.buffer, blob.bytes.byteOffset, blob.bytes.byteLength).toString(
|
|
716
|
+
"base64",
|
|
717
|
+
);
|
|
718
|
+
this.#record("out", "b", blobParams(blob), body);
|
|
595
719
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
720
|
+
this.#blobs.set(id, "sent");
|
|
721
|
+
}
|
|
722
|
+
if (!query?.timer) return;
|
|
723
|
+
query.timer.cancel();
|
|
724
|
+
query.timer = undefined;
|
|
725
|
+
this.#sendMissing(query.ids);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/** Send inline every id of `ids` still waiting on a query. */
|
|
729
|
+
#sendMissing(ids: readonly string[]): void {
|
|
730
|
+
for (const id of ids) {
|
|
731
|
+
const blob = this.#blobs.get(id);
|
|
732
|
+
if (blob !== undefined && blob !== "sent") this.#sendBlob(blob);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
#sendBlob(blob: NativeBlob): void {
|
|
737
|
+
this.#blobs.set(blob.id, "sent");
|
|
738
|
+
const body = Buffer.from(blob.bytes.buffer, blob.bytes.byteOffset, blob.bytes.byteLength).toString("base64");
|
|
739
|
+
// The full body, so a replay (Tern's `surface-play`) shows the image.
|
|
740
|
+
this.#record("out", "b", blobParams(blob), body);
|
|
741
|
+
this.#host.terminal.write(encodeTspMessage("b", body, blobParams(blob), this.#limit));
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* A new connection (resume, or a fresh inline surface after `gone`): the
|
|
746
|
+
* terminal is asked again. Outstanding queries stay, without their
|
|
747
|
+
* deadlines, only to pair their replies.
|
|
748
|
+
*/
|
|
749
|
+
#resetBlobs(): void {
|
|
750
|
+
this.#blobEpoch++;
|
|
751
|
+
this.#blobs.clear();
|
|
752
|
+
this.#askHeld = true;
|
|
753
|
+
for (const query of this.#blobQueries) {
|
|
754
|
+
query.timer?.cancel();
|
|
755
|
+
query.timer = undefined;
|
|
601
756
|
}
|
|
602
757
|
}
|
|
603
758
|
|
|
604
|
-
#write(verb: "o" | "f" | "t" | "x", body: unknown): void {
|
|
759
|
+
#write(verb: "o" | "f" | "t" | "x" | "q", body: unknown): void {
|
|
605
760
|
this.#record("out", verb, undefined, body);
|
|
606
761
|
this.#host.terminal.write(encodeTspJson(verb, body, undefined, this.#limit));
|
|
607
762
|
}
|
|
@@ -665,6 +820,7 @@ export class NativeBackend {
|
|
|
665
820
|
case "gone":
|
|
666
821
|
if (event.ids.includes(this.#inline.id)) {
|
|
667
822
|
// Adopt found nothing (evicted, or another pane): start over.
|
|
823
|
+
this.#resetBlobs();
|
|
668
824
|
this.#inline = this.#newSurface("inline");
|
|
669
825
|
if (this.#live) this.#open(this.#inline);
|
|
670
826
|
this.#host.requestRender();
|
package/src/native/blobs.d.ts
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* Content-addressed binary blobs (images) for `image` nodes.
|
|
3
3
|
*
|
|
4
4
|
* `describe()` registers the bytes and puts the returned id in `image.p.blob`;
|
|
5
|
-
* the native backend
|
|
6
|
-
*
|
|
5
|
+
* the native backend delivers each referenced blob once per terminal
|
|
6
|
+
* connection (asking the terminal first which it holds, or through its blob
|
|
7
|
+
* cache), else uploads it with verb `b` before the frame that first
|
|
8
|
+
* references it.
|
|
7
9
|
*/
|
|
8
10
|
import type { TspProps } from "@linxiraos/pi-wire";
|
|
9
11
|
import type { NativeNode } from "./node";
|
|
@@ -12,10 +14,12 @@ export interface NativeBlob {
|
|
|
12
14
|
readonly mime: string;
|
|
13
15
|
readonly bytes: Uint8Array;
|
|
14
16
|
}
|
|
15
|
-
/** Register
|
|
17
|
+
/** Register bytes for process-lifetime, id-only use. Prefer {@link nativeImageNode} for owned images. */
|
|
16
18
|
export declare function registerNativeBlob(bytes: Uint8Array, mime: string): string;
|
|
17
19
|
/** A registered blob by id. */
|
|
18
20
|
export declare function getNativeBlob(id: string): NativeBlob | undefined;
|
|
21
|
+
/** An image whose props own its bytes, including while hidden, unmounted, or awaiting upload. */
|
|
22
|
+
export declare function nativeImageNode(bytes: Uint8Array, mime: string, p?: Omit<TspProps<"image">, "blob" | "builtin">, key?: string): NativeNode;
|
|
19
23
|
/**
|
|
20
24
|
* An `image` node for a base64 payload: the bytes are registered as a blob and
|
|
21
25
|
* the pixel size probed from the header. Decoding and hashing cost real time,
|
package/src/native/blobs.ts
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* Content-addressed binary blobs (images) for `image` nodes.
|
|
3
3
|
*
|
|
4
4
|
* `describe()` registers the bytes and puts the returned id in `image.p.blob`;
|
|
5
|
-
* the native backend
|
|
6
|
-
*
|
|
5
|
+
* the native backend delivers each referenced blob once per terminal
|
|
6
|
+
* connection (asking the terminal first which it holds, or through its blob
|
|
7
|
+
* cache), else uploads it with verb `b` before the frame that first
|
|
8
|
+
* references it.
|
|
7
9
|
*/
|
|
8
10
|
import type { TspProps } from "@linxiraos/pi-wire";
|
|
9
11
|
import { getImageDimensions } from "../terminal-capabilities";
|
|
@@ -16,7 +18,13 @@ export interface NativeBlob {
|
|
|
16
18
|
readonly bytes: Uint8Array;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
const blobs = new Map<string, NativeBlob
|
|
21
|
+
const blobs = new Map<string, WeakRef<NativeBlob>>();
|
|
22
|
+
const registeredBlobs = new Map<string, NativeBlob>();
|
|
23
|
+
const releasedBlobs = new FinalizationRegistry<{ id: string; reference: WeakRef<NativeBlob> }>(({ id, reference }) => {
|
|
24
|
+
if (blobs.get(id) === reference) blobs.delete(id);
|
|
25
|
+
});
|
|
26
|
+
// Props spreads preserve ownership; wire serialization omits symbol keys.
|
|
27
|
+
const kBlob = Symbol("native.blob");
|
|
20
28
|
// Registration usually repeats with the same bytes object on every describe;
|
|
21
29
|
// the tag skips rehashing it.
|
|
22
30
|
const kBlobId = Symbol("native.blobId");
|
|
@@ -25,19 +33,42 @@ interface BlobTagged {
|
|
|
25
33
|
[kBlobId]?: string;
|
|
26
34
|
}
|
|
27
35
|
|
|
28
|
-
|
|
29
|
-
export function registerNativeBlob(bytes: Uint8Array, mime: string): string {
|
|
36
|
+
function getOrCreateBlob(bytes: Uint8Array, mime: string): NativeBlob {
|
|
30
37
|
const known = (bytes as BlobTagged)[kBlobId];
|
|
31
|
-
|
|
32
|
-
const id = new Bun.CryptoHasher("sha256").update(bytes).digest("hex");
|
|
38
|
+
const id = known ?? new Bun.CryptoHasher("sha256").update(bytes).digest("hex");
|
|
33
39
|
(bytes as BlobTagged)[kBlobId] = id;
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
let blob = blobs.get(id)?.deref();
|
|
41
|
+
if (!blob) {
|
|
42
|
+
blob = { id, mime, bytes };
|
|
43
|
+
const reference = new WeakRef(blob);
|
|
44
|
+
blobs.set(id, reference);
|
|
45
|
+
releasedBlobs.register(blob, { id, reference });
|
|
46
|
+
}
|
|
47
|
+
return blob;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Register bytes for process-lifetime, id-only use. Prefer {@link nativeImageNode} for owned images. */
|
|
51
|
+
export function registerNativeBlob(bytes: Uint8Array, mime: string): string {
|
|
52
|
+
const blob = getOrCreateBlob(bytes, mime);
|
|
53
|
+
registeredBlobs.set(blob.id, blob);
|
|
54
|
+
return blob.id;
|
|
36
55
|
}
|
|
37
56
|
|
|
38
57
|
/** A registered blob by id. */
|
|
39
58
|
export function getNativeBlob(id: string): NativeBlob | undefined {
|
|
40
|
-
return blobs.get(id);
|
|
59
|
+
return blobs.get(id)?.deref();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** An image whose props own its bytes, including while hidden, unmounted, or awaiting upload. */
|
|
63
|
+
export function nativeImageNode(
|
|
64
|
+
bytes: Uint8Array,
|
|
65
|
+
mime: string,
|
|
66
|
+
p?: Omit<TspProps<"image">, "blob" | "builtin">,
|
|
67
|
+
key?: string,
|
|
68
|
+
): NativeNode {
|
|
69
|
+
const blob = getOrCreateBlob(bytes, mime);
|
|
70
|
+
const props = { ...p, blob: blob.id, [kBlob]: blob };
|
|
71
|
+
return node("image", props, undefined, key);
|
|
41
72
|
}
|
|
42
73
|
|
|
43
74
|
/**
|
|
@@ -51,9 +82,13 @@ export function base64ImageNode(
|
|
|
51
82
|
p?: Omit<TspProps<"image">, "blob" | "builtin" | "w" | "h">,
|
|
52
83
|
key?: string,
|
|
53
84
|
): NativeNode {
|
|
54
|
-
const blob = registerNativeBlob(Buffer.from(data, "base64"), mimeType);
|
|
55
85
|
const size = getImageDimensions(data, mimeType);
|
|
56
|
-
return
|
|
86
|
+
return nativeImageNode(
|
|
87
|
+
Buffer.from(data, "base64"),
|
|
88
|
+
mimeType,
|
|
89
|
+
size ? { ...p, w: size.widthPx, h: size.heightPx } : p,
|
|
90
|
+
key,
|
|
91
|
+
);
|
|
57
92
|
}
|
|
58
93
|
|
|
59
94
|
/** `image` nodes for base64 payloads, one per slot key, rebuilt only when the slot's payload changes. */
|
package/src/native/encode.d.ts
CHANGED
|
@@ -15,11 +15,10 @@ export type TspIncoming = {
|
|
|
15
15
|
};
|
|
16
16
|
/** Message parameters (`k=v`, ASCII, never containing `;`). */
|
|
17
17
|
export type TspParams = Readonly<Record<string, string | number>>;
|
|
18
|
-
/** Split `body` into pieces of at most `limit` UTF-8 bytes, never inside a code point. */
|
|
19
|
-
export declare function splitUtf8(body: string, limit: number): string[];
|
|
20
18
|
/**
|
|
21
|
-
* Encode one logical message, chunked when `body` exceeds `limit` UTF-8 bytes
|
|
22
|
-
*
|
|
19
|
+
* Encode one logical message, chunked when `body` exceeds `limit` UTF-8 bytes:
|
|
20
|
+
* the first chunk carries `params`, every chunk `c=<id>` and all but the last
|
|
21
|
+
* `m=1`. Returns the complete byte string to write (all chunks, in order).
|
|
23
22
|
*/
|
|
24
23
|
export declare function encodeTspMessage(verb: TspVerb, body: string, params?: TspParams, limit?: number): string;
|
|
25
24
|
/** Encode a JSON message body. */
|
package/src/native/encode.ts
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* negotiated `apc` limit is split across messages with the same verb, tagged
|
|
7
7
|
* `c=<chunk-id>`, with `m=1` on every chunk but the last; the receiver joins
|
|
8
8
|
* the bodies byte-wise. Chunks split on code-point boundaries, so every chunk
|
|
9
|
-
* is valid UTF-8 on its own and the joined bytes equal the original body
|
|
9
|
+
* is valid UTF-8 on its own and the joined bytes equal the original body, and
|
|
10
|
+
* never where the next chunk would lead with a `key=value;` segment the
|
|
11
|
+
* receiver would read as a parameter (Tern's `tsp_chunks`).
|
|
10
12
|
*/
|
|
11
13
|
import { isRecord } from "@linxiraos/pi-utils/type-guards";
|
|
12
14
|
import {
|
|
@@ -34,36 +36,58 @@ export type TspParams = Readonly<Record<string, string | number>>;
|
|
|
34
36
|
|
|
35
37
|
let nextChunkId = 1;
|
|
36
38
|
|
|
37
|
-
/**
|
|
38
|
-
function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
/** Whether `bytes` from `at` leads with a `key=value;` segment, which a receiver reads as one more parameter. */
|
|
40
|
+
function leadsWithParameter(bytes: Uint8Array, at: number): boolean {
|
|
41
|
+
let i = at;
|
|
42
|
+
// Key bytes: `[A-Za-z0-9_-]`.
|
|
43
|
+
for (; i < bytes.length; i++) {
|
|
44
|
+
const b = bytes[i]!;
|
|
45
|
+
if (
|
|
46
|
+
!((b >= 0x30 && b <= 0x39) || (b >= 0x41 && b <= 0x5a) || (b >= 0x61 && b <= 0x7a) || b === 0x5f || b === 0x2d)
|
|
47
|
+
)
|
|
48
|
+
break;
|
|
45
49
|
}
|
|
46
|
-
|
|
50
|
+
if (i === at || bytes[i] !== 0x3d) return false;
|
|
51
|
+
i++;
|
|
52
|
+
// Value bytes: printable ASCII other than `;`.
|
|
53
|
+
while (i < bytes.length && bytes[i]! >= 0x21 && bytes[i]! <= 0x7e && bytes[i] !== 0x3b) i++;
|
|
54
|
+
return bytes[i] === 0x3b;
|
|
47
55
|
}
|
|
48
56
|
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Where the chunk of `bytes` starting at `start` ends: the safe cut nearest
|
|
59
|
+
* below `start + limit` (above `start`), else the nearest above it, else the
|
|
60
|
+
* end. A cut is safe on a UTF-8 character boundary where the rest doesn't
|
|
61
|
+
* lead with a parameter-shaped segment.
|
|
62
|
+
*/
|
|
63
|
+
function chunkEnd(bytes: Uint8Array, start: number, limit: number): number {
|
|
64
|
+
const target = start + limit;
|
|
65
|
+
if (target >= bytes.length) return bytes.length;
|
|
66
|
+
const safe = (at: number): boolean => (bytes[at]! & 0xc0) !== 0x80 && !leadsWithParameter(bytes, at);
|
|
67
|
+
for (let at = target; at > start; at--) if (safe(at)) return at;
|
|
68
|
+
for (let at = target + 1; at < bytes.length; at++) if (safe(at)) return at;
|
|
69
|
+
return bytes.length;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Chunk bodies for `body` of `byteLength` UTF-8 bytes, cut as Tern's
|
|
74
|
+
* `tsp_chunks` cuts. Pure ASCII without `;` (every base64 blob) has no
|
|
75
|
+
* parameter-shaped segment and no multibyte character, so it is cut every
|
|
76
|
+
* `limit` characters.
|
|
77
|
+
*/
|
|
78
|
+
function chunkBodies(body: string, byteLength: number, limit: number): string[] {
|
|
79
|
+
const max = Math.max(1, Math.trunc(limit));
|
|
52
80
|
const pieces: string[] = [];
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
bytes += step.bytes;
|
|
64
|
-
i += step.units;
|
|
81
|
+
if (byteLength === body.length && !body.includes(";")) {
|
|
82
|
+
for (let at = 0; at < body.length; at += max) pieces.push(body.slice(at, at + max));
|
|
83
|
+
return pieces;
|
|
84
|
+
}
|
|
85
|
+
const bytes = Buffer.from(body, "utf8");
|
|
86
|
+
for (let start = 0; start < bytes.length;) {
|
|
87
|
+
const end = chunkEnd(bytes, start, max);
|
|
88
|
+
pieces.push(bytes.toString("utf8", start, end));
|
|
89
|
+
start = end;
|
|
65
90
|
}
|
|
66
|
-
pieces.push(body.slice(start));
|
|
67
91
|
return pieces;
|
|
68
92
|
}
|
|
69
93
|
|
|
@@ -79,8 +103,9 @@ function encodeParams(params: TspParams | undefined): string {
|
|
|
79
103
|
}
|
|
80
104
|
|
|
81
105
|
/**
|
|
82
|
-
* Encode one logical message, chunked when `body` exceeds `limit` UTF-8 bytes
|
|
83
|
-
*
|
|
106
|
+
* Encode one logical message, chunked when `body` exceeds `limit` UTF-8 bytes:
|
|
107
|
+
* the first chunk carries `params`, every chunk `c=<id>` and all but the last
|
|
108
|
+
* `m=1`. Returns the complete byte string to write (all chunks, in order).
|
|
84
109
|
*/
|
|
85
110
|
export function encodeTspMessage(
|
|
86
111
|
verb: TspVerb,
|
|
@@ -90,13 +115,15 @@ export function encodeTspMessage(
|
|
|
90
115
|
): string {
|
|
91
116
|
const base = encodeParams(params);
|
|
92
117
|
// Fast path: ASCII-length bound first, exact byte count only near the limit.
|
|
93
|
-
if (body.length * 3 <= limit
|
|
118
|
+
if (body.length * 3 <= limit) return frame(verb, base, body);
|
|
119
|
+
const byteLength = Buffer.byteLength(body, "utf8");
|
|
120
|
+
if (byteLength <= limit) return frame(verb, base, body);
|
|
94
121
|
const chunkId = (nextChunkId++).toString(36);
|
|
95
|
-
const pieces =
|
|
122
|
+
const pieces = chunkBodies(body, byteLength, limit);
|
|
96
123
|
let out = "";
|
|
97
124
|
for (let i = 0; i < pieces.length; i++) {
|
|
98
125
|
const more = i < pieces.length - 1 ? ";m=1" : "";
|
|
99
|
-
out += frame(verb, `${base};c=${chunkId}${more}`, pieces[i]!);
|
|
126
|
+
out += frame(verb, `${i === 0 ? base : ""};c=${chunkId}${more}`, pieces[i]!);
|
|
100
127
|
}
|
|
101
128
|
return out;
|
|
102
129
|
}
|
package/src/native/overlay.d.ts
CHANGED
|
@@ -58,6 +58,11 @@ export interface ActionButtonOptions {
|
|
|
58
58
|
* which the component routes to the same code path as the key.
|
|
59
59
|
*/
|
|
60
60
|
export declare function actionButton(label: string, act: string, options?: ActionButtonOptions): NativeNode;
|
|
61
|
+
/**
|
|
62
|
+
* A bare `esc` keycap button for a view's top-right corner: a click sends
|
|
63
|
+
* `act` (default `close`), which the component routes to its Esc path.
|
|
64
|
+
*/
|
|
65
|
+
export declare function escCloseButton(act?: string, key?: string): NativeNode;
|
|
61
66
|
/** A row of {@link actionButton}s (role `zeta.actions`); `null` entries become the spacer that end-aligns what follows. */
|
|
62
67
|
export declare function actionBar(buttons: readonly (NativeNode | null)[], key?: string): NativeNode;
|
|
63
68
|
/** Spans of an annotation callout: a dim `label:` then the note. */
|
package/src/native/overlay.ts
CHANGED
|
@@ -119,6 +119,19 @@ export function actionButton(label: string, act: string, options: ActionButtonOp
|
|
|
119
119
|
);
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/**
|
|
123
|
+
* A bare `esc` keycap button for a view's top-right corner: a click sends
|
|
124
|
+
* `act` (default `close`), which the component routes to its Esc path.
|
|
125
|
+
*/
|
|
126
|
+
export function escCloseButton(act = "close", key = "esc-close"): NativeNode {
|
|
127
|
+
return node(
|
|
128
|
+
"row",
|
|
129
|
+
{ role: "zeta.btn", align: "center", actions: { click: act }, title: `Close ${formatTooltipKey("escape")}` },
|
|
130
|
+
[kbd("escape")],
|
|
131
|
+
key,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
122
135
|
/** A row of {@link actionButton}s (role `zeta.actions`); `null` entries become the spacer that end-aligns what follows. */
|
|
123
136
|
export function actionBar(buttons: readonly (NativeNode | null)[], key = "actions"): NativeNode {
|
|
124
137
|
const children = buttons.map(button => button ?? node("spacer", { grow: 1 }));
|
package/src/native/reconcile.ts
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import * as logger from "@linxiraos/pi-utils/logger";
|
|
31
31
|
import { TSP_TEXT_KINDS, type TspKind, type TspNode, type TspOp, type TspScrollBy } from "@linxiraos/pi-wire";
|
|
32
32
|
import { type Component, Container, CURSOR_MARKER } from "../tui";
|
|
33
|
+
import { getNativeBlob } from "./blobs";
|
|
33
34
|
import { normalizeIconProps } from "./icons";
|
|
34
35
|
import type { DescribeContext, NativeChild, NativeNode, NativeRevealAt } from "./node";
|
|
35
36
|
import { isNativeSettled } from "./settle";
|
|
@@ -291,6 +292,7 @@ export class Reconciler {
|
|
|
291
292
|
this.#prevPortals = portals;
|
|
292
293
|
this.#regions = next;
|
|
293
294
|
for (const entry of this.#dels) this.#deleteEntry(entry);
|
|
295
|
+
this.#dels = [];
|
|
294
296
|
// Parked to survive a parent/child swap but absent from the new description.
|
|
295
297
|
for (const state of this.#parked) {
|
|
296
298
|
if (!state.parked) continue;
|
|
@@ -711,6 +713,8 @@ export class Reconciler {
|
|
|
711
713
|
owner: Owner,
|
|
712
714
|
entries: readonly Entry[],
|
|
713
715
|
): Readonly<Record<string, unknown>> | undefined {
|
|
716
|
+
// Dereferencing pins the blob through this synchronous reconcile/upload job, even after settling drops its node.
|
|
717
|
+
if (node.k === "image" && node.p?.blob) getNativeBlob(node.p.blob);
|
|
714
718
|
return normalizeIconProps(node.k, this.#resolveRefs(node, owner, entries));
|
|
715
719
|
}
|
|
716
720
|
|