@linxiraos/pi-tui 1.1.26 → 1.2.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 +51 -491
- package/README.md +13 -7
- package/dist/types/active-terminal.d.ts +30 -0
- package/dist/types/app-keybindings.d.ts +7 -2
- 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 +5 -4
- 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 +11 -1
- package/dist/types/chat/chat-transcript-builder.d.ts +3 -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 +17 -4
- package/dist/types/chrome/shared.d.ts +9 -0
- package/dist/types/chrome/status-notice.d.ts +15 -3
- 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 +23 -5
- package/dist/types/components/form.d.ts +1 -0
- package/dist/types/components/image.d.ts +26 -44
- package/dist/types/components/input.d.ts +2 -1
- 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/loader.d.ts +5 -6
- package/dist/types/components/markdown.d.ts +47 -5
- 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 +9 -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/apply.d.ts +4 -3
- package/dist/types/native/backend.d.ts +5 -4
- package/dist/types/native/blobs.d.ts +7 -3
- package/dist/types/native/encode.d.ts +8 -6
- package/dist/types/native/node.d.ts +36 -5
- package/dist/types/native/overlay.d.ts +8 -3
- package/dist/types/native/settle.d.ts +4 -3
- 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 +3 -0
- package/dist/types/overlays/agent-transcript-viewer.d.ts +12 -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/btw-history-panel.d.ts +13 -1
- package/dist/types/overlays/copy-selector.d.ts +1 -0
- package/dist/types/overlays/history-search.d.ts +10 -0
- package/dist/types/overlays/hook-selector.d.ts +9 -4
- package/dist/types/overlays/hub-frame.d.ts +4 -1
- 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 +37 -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/report-panel.d.ts +68 -0
- package/dist/types/overlays/rewind-selector.d.ts +1 -0
- 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/thinking-selector.d.ts +2 -2
- package/dist/types/overlays/usage-dashboard.d.ts +12 -8
- package/dist/types/overlays/usage-display.d.ts +3 -1
- package/dist/types/prompt/composer-cache.d.ts +0 -16
- package/dist/types/prompt/composer.d.ts +18 -3
- package/dist/types/prompt/custom-editor.d.ts +25 -13
- package/dist/types/prompt/image-source.d.ts +2 -1
- package/dist/types/prompt/interactive-context-helpers.d.ts +5 -1
- package/dist/types/prompt/model-mention-autocomplete.d.ts +1 -1
- package/dist/types/prompt/welcome.d.ts +7 -2
- 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/types.d.ts +1 -0
- package/dist/types/space-hold.d.ts +40 -25
- package/dist/types/status-line/component.d.ts +3 -0
- package/dist/types/status-line/context-usage.d.ts +7 -5
- 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 +41 -36
- package/dist/types/terminal-multiplexer.d.ts +127 -14
- package/dist/types/terminal.d.ts +23 -16
- 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/thinking.d.ts +1 -1
- package/dist/types/tmux.d.ts +11 -4
- package/dist/types/tools/bash.d.ts +7 -2
- 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 +2 -3
- package/dist/types/tools/write.d.ts +9 -2
- package/dist/types/tools/yield.d.ts +22 -0
- package/dist/types/tui.d.ts +36 -0
- package/dist/types/utils.d.ts +6 -0
- package/dist/types/vim.d.ts +19 -5
- package/dist/types/windows-input-mode.d.ts +33 -1
- package/package.json +13 -9
- package/src/active-terminal.d.ts +30 -0
- package/src/active-terminal.ts +80 -0
- package/src/app-keybindings.d.ts +7 -2
- package/src/app-keybindings.ts +7 -2
- 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 +96 -23
- package/src/apps/git/git-tui.ts +23 -16
- package/src/apps/git/help.ts +29 -20
- package/src/apps/git/sidebar.ts +8 -3
- 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/bracketed-paste.ts +10 -3
- 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 +11 -1
- package/src/chat/assistant-message.ts +245 -54
- package/src/chat/chat-transcript-builder.d.ts +3 -0
- package/src/chat/chat-transcript-builder.ts +9 -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 +86 -7
- package/src/chat/user-message.d.ts +17 -4
- package/src/chat/user-message.ts +71 -8
- package/src/chrome/shared.ts +53 -0
- package/src/chrome/status-notice.ts +25 -7
- package/src/chrome/transcript-container.d.ts +6 -0
- package/src/chrome/transcript-container.ts +162 -19
- 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 +23 -5
- package/src/components/editor.ts +316 -47
- package/src/components/form.d.ts +1 -0
- package/src/components/form.ts +8 -0
- package/src/components/image.d.ts +26 -44
- package/src/components/image.ts +115 -152
- package/src/components/input.d.ts +2 -1
- package/src/components/input.ts +17 -7
- 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/loader.d.ts +5 -6
- package/src/components/loader.ts +10 -17
- package/src/components/markdown.d.ts +47 -5
- package/src/components/markdown.ts +786 -303
- 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 +3 -2
- package/src/fuzzy.d.ts +29 -1
- package/src/fuzzy.ts +115 -30
- package/src/hotkeys-markdown.ts +9 -2
- package/src/key-hint-format.d.ts +9 -1
- package/src/key-hint-format.ts +26 -11
- 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 +11 -5
- 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/apply.d.ts +4 -3
- package/src/native/apply.ts +4 -3
- package/src/native/backend.d.ts +5 -4
- package/src/native/backend.ts +199 -22
- package/src/native/blobs.d.ts +7 -3
- package/src/native/blobs.ts +47 -12
- package/src/native/encode.d.ts +8 -6
- package/src/native/encode.ts +76 -34
- package/src/native/node.d.ts +36 -5
- package/src/native/node.ts +31 -5
- package/src/native/overlay.d.ts +8 -3
- package/src/native/overlay.ts +17 -4
- package/src/native/reconcile.ts +16 -4
- package/src/native/settle.d.ts +4 -3
- package/src/native/settle.ts +4 -3
- 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 +48 -29
- package/src/overlays/agent-transcript-viewer.ts +93 -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 +10 -17
- package/src/overlays/btw-history-panel.ts +136 -39
- package/src/overlays/copy-selector.d.ts +1 -0
- package/src/overlays/copy-selector.ts +32 -8
- package/src/overlays/extensions/extension-dashboard.ts +4 -4
- 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/hub-frame.ts +19 -1
- package/src/overlays/jobs-panel.ts +33 -52
- package/src/overlays/model-browser.ts +367 -122
- package/src/overlays/model-hub.ts +435 -59
- package/src/overlays/move-overlay.ts +28 -14
- package/src/overlays/oauth-selector.ts +54 -70
- package/src/overlays/pause-screen.ts +2 -2
- package/src/overlays/plan-review-overlay.ts +208 -70
- package/src/overlays/plugin-settings.ts +3 -3
- package/src/overlays/report-panel.ts +191 -0
- package/src/overlays/rewind-selector.ts +88 -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 +5 -4
- 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/thinking-selector.ts +11 -10
- package/src/overlays/tree-selector.ts +41 -20
- package/src/overlays/usage-dashboard.ts +117 -58
- package/src/overlays/usage-display.ts +10 -4
- package/src/overlays/usage-row.ts +22 -8
- package/src/prompt/attachment-chips.ts +15 -11
- package/src/prompt/composer-attachments.ts +6 -4
- package/src/prompt/composer-cache.ts +7 -86
- package/src/prompt/composer.ts +37 -26
- package/src/prompt/custom-editor.ts +282 -105
- package/src/prompt/image-source.ts +2 -1
- package/src/prompt/interactive-context-helpers.ts +32 -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/queued-messages.ts +6 -4
- package/src/prompt/tips.txt +13 -9
- package/src/prompt/welcome.ts +64 -46
- 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 +16 -1
- 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/types.ts +1 -0
- package/src/space-hold.d.ts +40 -25
- package/src/space-hold.ts +156 -52
- package/src/status-line/component.ts +292 -78
- package/src/status-line/context-usage.ts +21 -46
- 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 +41 -36
- package/src/terminal-capabilities.ts +137 -213
- package/src/terminal-multiplexer.d.ts +127 -14
- package/src/terminal-multiplexer.ts +112 -33
- package/src/terminal.d.ts +23 -16
- package/src/terminal.ts +203 -99
- 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 +11 -4
- 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/thinking.d.ts +1 -1
- package/src/thinking.ts +1 -1
- 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.d.ts +7 -2
- package/src/tools/bash.ts +116 -47
- 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 +154 -50
- package/src/tools/task.d.ts +2 -3
- package/src/tools/task.ts +115 -62
- 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 +36 -0
- package/src/tui.ts +666 -217
- 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.d.ts +33 -1
- package/src/windows-input-mode.ts +212 -6
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { TERMINAL } from "../terminal-capabilities";
|
|
2
|
+
import { extractSegments, sliceByColumn, sliceWithWidth, visibleWidth } from "../utils";
|
|
3
|
+
|
|
4
|
+
const SEGMENT_RESET = "\x1b[0m";
|
|
5
|
+
|
|
6
|
+
/** Splice overlay content into a base line at a specific column. Single-pass optimized. */
|
|
7
|
+
export function compositeLineAt(
|
|
8
|
+
baseLine: string,
|
|
9
|
+
overlayLine: string,
|
|
10
|
+
startCol: number,
|
|
11
|
+
overlayWidth: number,
|
|
12
|
+
totalWidth: number,
|
|
13
|
+
): string {
|
|
14
|
+
// A full-width image row owns its terminal row; the writer must see its exact placement shape.
|
|
15
|
+
if (startCol === 0 && overlayWidth >= totalWidth && TERMINAL.isImageLine(overlayLine)) return overlayLine;
|
|
16
|
+
if (TERMINAL.isImageLine(baseLine)) {
|
|
17
|
+
// Full-width overlays such as /switch are opaque: replace the
|
|
18
|
+
// Unicode placeholder cells so the image cannot cover the modal.
|
|
19
|
+
// Partial overlays cannot safely splice placement control sequences.
|
|
20
|
+
if (startCol !== 0 || overlayWidth < totalWidth) return baseLine;
|
|
21
|
+
const overlay = sliceWithWidth(overlayLine, 0, totalWidth, true);
|
|
22
|
+
return SEGMENT_RESET + overlay.text + " ".repeat(Math.max(0, totalWidth - overlay.width));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Single pass through baseLine extracts both before and after segments
|
|
26
|
+
const afterStart = startCol + overlayWidth;
|
|
27
|
+
const base = extractSegments(baseLine, startCol, afterStart, totalWidth - afterStart, true);
|
|
28
|
+
|
|
29
|
+
// Extract overlay with width tracking (strict=true to exclude wide chars at boundary)
|
|
30
|
+
const overlay = sliceWithWidth(overlayLine, 0, overlayWidth, true);
|
|
31
|
+
|
|
32
|
+
// Pad segments to target widths
|
|
33
|
+
const beforePad = Math.max(0, startCol - base.beforeWidth);
|
|
34
|
+
const overlayPad = Math.max(0, overlayWidth - overlay.width);
|
|
35
|
+
const actualBeforeWidth = Math.max(startCol, base.beforeWidth);
|
|
36
|
+
const actualOverlayWidth = Math.max(overlayWidth, overlay.width);
|
|
37
|
+
const afterTarget = Math.max(0, totalWidth - actualBeforeWidth - actualOverlayWidth);
|
|
38
|
+
const afterPad = Math.max(0, afterTarget - base.afterWidth);
|
|
39
|
+
|
|
40
|
+
// Compose result
|
|
41
|
+
const r = SEGMENT_RESET;
|
|
42
|
+
const result =
|
|
43
|
+
base.before +
|
|
44
|
+
" ".repeat(beforePad) +
|
|
45
|
+
r +
|
|
46
|
+
overlay.text +
|
|
47
|
+
" ".repeat(overlayPad) +
|
|
48
|
+
r +
|
|
49
|
+
base.after +
|
|
50
|
+
" ".repeat(afterPad);
|
|
51
|
+
|
|
52
|
+
// CRITICAL: Always verify and truncate to terminal width.
|
|
53
|
+
// This is the final safeguard against width overflow which would crash the TUI.
|
|
54
|
+
// Width tracking can drift from actual visible width due to:
|
|
55
|
+
// - Complex ANSI/OSC sequences (hyperlinks, colors)
|
|
56
|
+
// - Wide characters at segment boundaries
|
|
57
|
+
// - Edge cases in segment extraction
|
|
58
|
+
const resultWidth = visibleWidth(result);
|
|
59
|
+
if (resultWidth <= totalWidth) {
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
// Truncate with strict=true to ensure we don't exceed totalWidth
|
|
63
|
+
return sliceByColumn(result, 0, totalWidth, true);
|
|
64
|
+
}
|
|
@@ -92,6 +92,7 @@ export declare class OutputPane implements Component {
|
|
|
92
92
|
describe(_cx: DescribeContext): NativeNode;
|
|
93
93
|
render(width: number): readonly string[];
|
|
94
94
|
invalidate(): void;
|
|
95
|
+
releaseRenderCaches(): void;
|
|
95
96
|
}
|
|
96
97
|
/** Default styling for plain tool output while preserving existing ANSI. */
|
|
97
98
|
export declare function styleToolOutputLine(line: string, theme: Theme): string;
|
|
@@ -302,6 +302,12 @@ export class OutputPane implements Component {
|
|
|
302
302
|
this.#text.invalidate();
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
+
releaseRenderCaches(): void {
|
|
306
|
+
this.#renderKey = "";
|
|
307
|
+
this.#text.setText("");
|
|
308
|
+
this.#text.releaseRenderCaches();
|
|
309
|
+
}
|
|
310
|
+
|
|
305
311
|
#appendToTail(text: string): void {
|
|
306
312
|
if (!text) return;
|
|
307
313
|
if (this.#lines.length === 0) this.#lines.push("");
|
|
@@ -298,6 +298,7 @@ export interface RenderedStringCache {
|
|
|
298
298
|
export declare function createRenderedStringCache(): RenderedStringCache;
|
|
299
299
|
/** Drop the memo so the next lookup re-renders (e.g. the render function identity changed). */
|
|
300
300
|
export declare function invalidateRenderedStringCache(cache: RenderedStringCache): void;
|
|
301
|
+
export declare function releaseRenderedStringCache(cache: RenderedStringCache): void;
|
|
301
302
|
/** Reuse a rendered string while its theme, expansion, content, and salt match. */
|
|
302
303
|
export declare function cachedRenderedString(cache: RenderedStringCache | undefined, theme: Theme, expanded: boolean, salt: string, content: string, render: () => string): string;
|
|
303
304
|
/**
|
|
@@ -1042,8 +1042,12 @@ export function formatToolWorkingDirectory(workdir: string | undefined, projectD
|
|
|
1042
1042
|
return undefined;
|
|
1043
1043
|
}
|
|
1044
1044
|
const relativePath = path.relative(resolvedProjectDir, resolvedWorkdir);
|
|
1045
|
+
// On Windows, `path.relative` across drives returns the absolute target, which is not inside the project.
|
|
1045
1046
|
const isWithinProject =
|
|
1046
|
-
relativePath.length > 0 &&
|
|
1047
|
+
relativePath.length > 0 &&
|
|
1048
|
+
!path.isAbsolute(relativePath) &&
|
|
1049
|
+
!relativePath.startsWith("..") &&
|
|
1050
|
+
!relativePath.startsWith(`..${path.sep}`);
|
|
1047
1051
|
const displayWorkdir = isWithinProject ? relativePath : shortenPath(resolvedWorkdir);
|
|
1048
1052
|
return replaceTabs(displayWorkdir);
|
|
1049
1053
|
}
|
|
@@ -1124,6 +1128,9 @@ export function createCachedComponent(
|
|
|
1124
1128
|
invalidate() {
|
|
1125
1129
|
cached = undefined;
|
|
1126
1130
|
},
|
|
1131
|
+
releaseRenderCaches() {
|
|
1132
|
+
cached = undefined;
|
|
1133
|
+
},
|
|
1127
1134
|
};
|
|
1128
1135
|
}
|
|
1129
1136
|
|
|
@@ -1156,6 +1163,14 @@ export function invalidateRenderedStringCache(cache: RenderedStringCache): void
|
|
|
1156
1163
|
cache.theme = null;
|
|
1157
1164
|
}
|
|
1158
1165
|
|
|
1166
|
+
export function releaseRenderedStringCache(cache: RenderedStringCache): void {
|
|
1167
|
+
cache.theme = null;
|
|
1168
|
+
cache.expanded = false;
|
|
1169
|
+
cache.salt = "";
|
|
1170
|
+
cache.content = "";
|
|
1171
|
+
cache.value = "";
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1159
1174
|
/** Reuse a rendered string while its theme, expansion, content, and salt match. */
|
|
1160
1175
|
export function cachedRenderedString(
|
|
1161
1176
|
cache: RenderedStringCache | undefined,
|
|
@@ -46,6 +46,11 @@ export interface ToolCardOptions {
|
|
|
46
46
|
paddingY?: number;
|
|
47
47
|
ignoreTight?: boolean;
|
|
48
48
|
onInvalidate?: () => void;
|
|
49
|
+
/**
|
|
50
|
+
* Drop builder-owned memos the next build recomputes. Called by
|
|
51
|
+
* {@link ToolCard.releaseRenderCaches}, which never calls `onInvalidate`.
|
|
52
|
+
*/
|
|
53
|
+
onReleaseRenderCaches?: () => void;
|
|
49
54
|
onDispose?: () => void;
|
|
50
55
|
}
|
|
51
56
|
/** Map transcript lifecycle terminology onto the output frame's visual states. */
|
|
@@ -71,6 +76,7 @@ export declare class ToolCard implements Component {
|
|
|
71
76
|
handleInput(data: string): void;
|
|
72
77
|
setIgnoreTight(ignore: boolean): void;
|
|
73
78
|
invalidate(): void;
|
|
79
|
+
releaseRenderCaches(): void;
|
|
74
80
|
dispose(): void;
|
|
75
81
|
}
|
|
76
82
|
/** Construct a lazy framed tool card. */
|
package/src/render/tool-card.ts
CHANGED
|
@@ -69,6 +69,11 @@ export interface ToolCardOptions {
|
|
|
69
69
|
paddingY?: number;
|
|
70
70
|
ignoreTight?: boolean;
|
|
71
71
|
onInvalidate?: () => void;
|
|
72
|
+
/**
|
|
73
|
+
* Drop builder-owned memos the next build recomputes. Called by
|
|
74
|
+
* {@link ToolCard.releaseRenderCaches}, which never calls `onInvalidate`.
|
|
75
|
+
*/
|
|
76
|
+
onReleaseRenderCaches?: () => void;
|
|
72
77
|
onDispose?: () => void;
|
|
73
78
|
}
|
|
74
79
|
|
|
@@ -381,6 +386,16 @@ export class ToolCard implements Component {
|
|
|
381
386
|
this.#options.onInvalidate?.();
|
|
382
387
|
}
|
|
383
388
|
|
|
389
|
+
releaseRenderCaches(): void {
|
|
390
|
+
this.#block.invalidate();
|
|
391
|
+
this.#lastBlockOptions = undefined;
|
|
392
|
+
this.#plainKey = "";
|
|
393
|
+
this.#plainText.setText("");
|
|
394
|
+
this.#plainText.releaseRenderCaches();
|
|
395
|
+
for (const child of this.#renderedChildren) child.releaseRenderCaches?.();
|
|
396
|
+
this.#options.onReleaseRenderCaches?.();
|
|
397
|
+
}
|
|
398
|
+
|
|
384
399
|
dispose(): void {
|
|
385
400
|
if (this.#disposed) return;
|
|
386
401
|
this.#disposed = true;
|
|
@@ -20,6 +20,8 @@ export declare class WidthAwareText implements Component {
|
|
|
20
20
|
setCustomBgFn(customBgFn?: (text: string) => string): void;
|
|
21
21
|
setIgnoreTight(ignore: boolean): this;
|
|
22
22
|
invalidate(): void;
|
|
23
|
+
/** The formatted text stays: the inner `Text` renders from it, so dropping it would free nothing. */
|
|
24
|
+
releaseRenderCaches(): void;
|
|
23
25
|
/**
|
|
24
26
|
* Re-run the formatter on the next render because its inputs changed.
|
|
25
27
|
* Unlike {@link invalidate}, the inner `Text` keeps its wrap cache, so a
|
|
@@ -51,6 +51,11 @@ export class WidthAwareText implements Component {
|
|
|
51
51
|
this.#inner.invalidate();
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** The formatted text stays: the inner `Text` renders from it, so dropping it would free nothing. */
|
|
55
|
+
releaseRenderCaches(): void {
|
|
56
|
+
this.#inner.releaseRenderCaches();
|
|
57
|
+
}
|
|
58
|
+
|
|
54
59
|
/**
|
|
55
60
|
* Re-run the formatter on the next render because its inputs changed.
|
|
56
61
|
* Unlike {@link invalidate}, the inner `Text` keeps its wrap cache, so a
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Model } from "@linxiraos/pi-ai";
|
|
2
|
+
import { modelKind } from "@linxiraos/pi-catalog/types";
|
|
2
3
|
import type { SgrMouseEvent } from "../../mouse";
|
|
3
4
|
import { Text } from "../../components/text";
|
|
4
5
|
import { WizardStep } from "../../components/wizard-step";
|
|
@@ -114,7 +115,7 @@ class ModelSceneController implements SetupSceneController {
|
|
|
114
115
|
const { available, all, current } = this.#host.ctx.getModels();
|
|
115
116
|
const source = this.#host.ctx.modelSource;
|
|
116
117
|
const roles = resolveRoleAssignments(source, all, available);
|
|
117
|
-
const items = buildBrowserItems(available);
|
|
118
|
+
const items = buildBrowserItems(available.filter(model => modelKind(model) === "chat"));
|
|
118
119
|
sortModelItems(items, { roles, mruOrder: source.mruOrder });
|
|
119
120
|
this.#browser.setRoles(roles);
|
|
120
121
|
this.#browser.setMruOrder(source.mruOrder);
|
|
@@ -29,6 +29,7 @@ export interface SetupHost extends SetupUiHost {
|
|
|
29
29
|
readonly disabledProviders: readonly string[];
|
|
30
30
|
readonly authStorage: AuthStorage;
|
|
31
31
|
readonly modelSource: ModelBrowserSource;
|
|
32
|
+
/** Available and catalog models of every kind; role chips resolve over all of them, rows list chat models. */
|
|
32
33
|
getModels(): { available: Model[]; all: Model[]; current: Model | undefined };
|
|
33
34
|
refreshModels(): Promise<void>;
|
|
34
35
|
selectModel(model: Model, selector: string): Promise<void>;
|
package/src/space-hold.d.ts
CHANGED
|
@@ -1,45 +1,60 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { type KeyId } from "./keys";
|
|
2
|
+
/** Printable text a push-to-talk candidate key types (plain or shifted single characters and Space),
|
|
3
|
+
* or undefined for non-printable keys and chords, which the gesture reserves instead of typing. */
|
|
4
|
+
export declare function getSpaceHoldText(data: string, canonical: string | undefined): string | undefined;
|
|
5
|
+
/** Max gap (ms) between two configured-key presses for a later one to count as OS key auto-repeat
|
|
6
|
+
* rather than a deliberate press. OS auto-repeat is fast; a deliberate tap (even a fast one) is slower. */
|
|
3
7
|
export declare const SPACE_REPEAT_MAX_GAP_MS = 120;
|
|
4
|
-
/** Two consecutive inter-
|
|
8
|
+
/** Two consecutive inter-key gaps are "mechanical" (machine-driven auto-repeat) when both are
|
|
5
9
|
* within {@link SPACE_REPEAT_MAX_GAP_MS} and differ by no more than this — an absolute jitter floor
|
|
6
10
|
* or, for slower repeat rates, {@link SPACE_REPEAT_JITTER_RATIO} of the smaller gap. OS key-repeat
|
|
7
|
-
* is metronomic;
|
|
8
|
-
* steady. */
|
|
11
|
+
* is metronomic; human typing is irregular, so its deltas do not stay this steady. */
|
|
9
12
|
export declare const SPACE_REPEAT_JITTER_MS = 18;
|
|
10
13
|
export declare const SPACE_REPEAT_JITTER_RATIO = 0.35;
|
|
11
|
-
/** Consecutive mechanical (fast + steady) deltas that confirm
|
|
12
|
-
* recording. Needs a sustained metronomic cadence, so jittery
|
|
14
|
+
/** Consecutive mechanical (fast + steady) deltas that confirm a configured key is held and start
|
|
15
|
+
* recording. Needs a sustained metronomic cadence, so jittery typing and deliberate taps never
|
|
13
16
|
* reach it. */
|
|
14
17
|
export declare const SPACE_HOLD_MECHANICAL_RUN = 2;
|
|
15
|
-
/** Idle gap (ms) after the last repeated
|
|
16
|
-
*
|
|
18
|
+
/** Idle gap (ms) after the last repeated configured-key press that counts as release and ends
|
|
19
|
+
* push-to-talk. Must comfortably exceed the OS key-repeat interval. */
|
|
17
20
|
export declare const SPACE_HOLD_RELEASE_MS = 250;
|
|
18
|
-
/**
|
|
21
|
+
/** A release timer firing more than this (ms) past its deadline means the event loop stalled (a
|
|
22
|
+
* synchronous microphone open, GC, a heavy render) while the bar may still be held: the repeats
|
|
23
|
+
* emitted during the stall are queued but unread, so the stall must not read as a release. */
|
|
24
|
+
export declare const SPACE_HOLD_STALL_SLACK_MS = 100;
|
|
25
|
+
/** What a recognized push-to-talk hold drives — the start and stop callbacks. */
|
|
19
26
|
export interface SpaceHoldHandler {
|
|
20
|
-
/** Gate for the gesture. Returns false to
|
|
21
|
-
* disabling push-to-talk restores plain space behavior. */
|
|
27
|
+
/** Gate for the gesture. Returns false to let matching keys behave normally. */
|
|
22
28
|
enabled(): boolean;
|
|
23
|
-
/** A sustained hold was recognized
|
|
29
|
+
/** A sustained hold was recognized; any safely typed candidate text has already been deleted. */
|
|
24
30
|
onStart(): void;
|
|
25
|
-
/** The held
|
|
31
|
+
/** The held key was released (an idle gap with no further repeats, or any other key). */
|
|
26
32
|
onEnd(): void;
|
|
27
33
|
}
|
|
28
34
|
/** What the host must do with a keypress after {@link SpaceHoldGesture.process} has seen it:
|
|
29
|
-
* `"pass"` — handle it normally; `"type"` — insert
|
|
30
|
-
* turns into a hold); `"swallow"` — drop it, the gesture consumed it. */
|
|
35
|
+
* `"pass"` — handle it normally; `"type"` — insert its safely decoded printable text now (it is
|
|
36
|
+
* tracked back out if the run turns into a hold); `"swallow"` — drop it, the gesture consumed it. */
|
|
31
37
|
export type SpaceHoldStep = "pass" | "type" | "swallow";
|
|
32
|
-
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* taps are too slow, so neither escalates and both keep typing real spaces; the few spaces typed
|
|
37
|
-
* before a real hold is recognized are tracked back out of the host input. */
|
|
38
|
+
/** Push-to-talk key-hold state machine. A held key emits OS auto-repeat at a steady fast interval.
|
|
39
|
+
* We watch inter-key deltas and only recognize a hold once {@link SPACE_HOLD_MECHANICAL_RUN}
|
|
40
|
+
* consecutive deltas are mechanical (see {@link gapsAreMechanical}). Deliberate taps and jittery
|
|
41
|
+
* runs stay typed; only text typed during the current steady run is removed on recognition. */
|
|
38
42
|
export declare class SpaceHoldGesture {
|
|
39
43
|
#private;
|
|
40
|
-
/**
|
|
44
|
+
/** Configured alternative keys. Alternatives are tracked independently and never combine into one run. */
|
|
45
|
+
get keys(): readonly KeyId[];
|
|
46
|
+
set keys(keys: readonly KeyId[]);
|
|
47
|
+
/** Unset (the default) keeps configured keys behaving normally. */
|
|
41
48
|
handler: SpaceHoldHandler | undefined;
|
|
42
49
|
constructor(deleteTyped: (count: number) => void, allowed?: () => boolean);
|
|
43
|
-
/**
|
|
44
|
-
|
|
50
|
+
/** Pure focused-owner preflight on raw input: whether the key must reach the host before
|
|
51
|
+
* TUI-wide input listeners. Only non-printable bindings are claimed — they would otherwise
|
|
52
|
+
* trigger app shortcuts mid-hold; printable candidates keep the normal listener path so
|
|
53
|
+
* raw-input observers (e.g. extensions) still see typed text, as they did before remapping. */
|
|
54
|
+
shouldRoute(data: string): boolean;
|
|
55
|
+
/** Feed one canonical keypress and, when safely printable, its decoded text length. The host
|
|
56
|
+
* records the actual inserted length with {@link recordTyped} after applying normal typing. */
|
|
57
|
+
process(key: string | undefined, insertedCharacterLength?: number): SpaceHoldStep;
|
|
58
|
+
/** Account for text the host actually inserted for the preceding `"type"` step. */
|
|
59
|
+
recordTyped(insertedCharacterLength: number): void;
|
|
45
60
|
}
|
package/src/space-hold.ts
CHANGED
|
@@ -1,22 +1,40 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { addKeyAliases, canonicalKeyId } from "./keybindings";
|
|
2
|
+
import { extractPrintableText, type KeyId, parseKey } from "./keys";
|
|
3
|
+
|
|
4
|
+
/** Printable text a push-to-talk candidate key types (plain or shifted single characters and Space),
|
|
5
|
+
* or undefined for non-printable keys and chords, which the gesture reserves instead of typing. */
|
|
6
|
+
export function getSpaceHoldText(data: string, canonical: string | undefined): string | undefined {
|
|
7
|
+
if (canonical === undefined) return undefined;
|
|
8
|
+
const shifted = canonical.startsWith("shift+");
|
|
9
|
+
const base = shifted ? canonical.slice("shift+".length) : canonical;
|
|
10
|
+
if (base !== "space" && base.length !== 1) return undefined;
|
|
11
|
+
if (base === "space" && canonical !== "space" && !shifted) return undefined;
|
|
12
|
+
const text = extractPrintableText(data);
|
|
13
|
+
return text && (base !== "space" || text === " ") ? text : undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Max gap (ms) between two configured-key presses for a later one to count as OS key auto-repeat
|
|
17
|
+
* rather than a deliberate press. OS auto-repeat is fast; a deliberate tap (even a fast one) is slower. */
|
|
3
18
|
export const SPACE_REPEAT_MAX_GAP_MS = 120;
|
|
4
|
-
/** Two consecutive inter-
|
|
19
|
+
/** Two consecutive inter-key gaps are "mechanical" (machine-driven auto-repeat) when both are
|
|
5
20
|
* within {@link SPACE_REPEAT_MAX_GAP_MS} and differ by no more than this — an absolute jitter floor
|
|
6
21
|
* or, for slower repeat rates, {@link SPACE_REPEAT_JITTER_RATIO} of the smaller gap. OS key-repeat
|
|
7
|
-
* is metronomic;
|
|
8
|
-
* steady. */
|
|
22
|
+
* is metronomic; human typing is irregular, so its deltas do not stay this steady. */
|
|
9
23
|
export const SPACE_REPEAT_JITTER_MS = 18;
|
|
10
24
|
export const SPACE_REPEAT_JITTER_RATIO = 0.35;
|
|
11
|
-
/** Consecutive mechanical (fast + steady) deltas that confirm
|
|
12
|
-
* recording. Needs a sustained metronomic cadence, so jittery
|
|
25
|
+
/** Consecutive mechanical (fast + steady) deltas that confirm a configured key is held and start
|
|
26
|
+
* recording. Needs a sustained metronomic cadence, so jittery typing and deliberate taps never
|
|
13
27
|
* reach it. */
|
|
14
28
|
export const SPACE_HOLD_MECHANICAL_RUN = 2;
|
|
15
|
-
/** Idle gap (ms) after the last repeated
|
|
16
|
-
*
|
|
29
|
+
/** Idle gap (ms) after the last repeated configured-key press that counts as release and ends
|
|
30
|
+
* push-to-talk. Must comfortably exceed the OS key-repeat interval. */
|
|
17
31
|
export const SPACE_HOLD_RELEASE_MS = 250;
|
|
32
|
+
/** A release timer firing more than this (ms) past its deadline means the event loop stalled (a
|
|
33
|
+
* synchronous microphone open, GC, a heavy render) while the bar may still be held: the repeats
|
|
34
|
+
* emitted during the stall are queued but unread, so the stall must not read as a release. */
|
|
35
|
+
export const SPACE_HOLD_STALL_SLACK_MS = 100;
|
|
18
36
|
|
|
19
|
-
/** Whether two consecutive inter-
|
|
37
|
+
/** Whether two consecutive inter-key gaps look machine-driven: both within the auto-repeat band
|
|
20
38
|
* and steady enough (small absolute or proportional difference). OS key-repeat is metronomic, so
|
|
21
39
|
* its successive deltas match closely; human smashing is fast but irregular and deliberate taps are
|
|
22
40
|
* too slow, so neither passes. */
|
|
@@ -26,46 +44,62 @@ function gapsAreMechanical(gap: number, prevGap: number): boolean {
|
|
|
26
44
|
return Math.abs(gap - prevGap) <= tolerance;
|
|
27
45
|
}
|
|
28
46
|
|
|
29
|
-
/** What a recognized
|
|
47
|
+
/** What a recognized push-to-talk hold drives — the start and stop callbacks. */
|
|
30
48
|
export interface SpaceHoldHandler {
|
|
31
|
-
/** Gate for the gesture. Returns false to
|
|
32
|
-
* disabling push-to-talk restores plain space behavior. */
|
|
49
|
+
/** Gate for the gesture. Returns false to let matching keys behave normally. */
|
|
33
50
|
enabled(): boolean;
|
|
34
|
-
/** A sustained hold was recognized
|
|
51
|
+
/** A sustained hold was recognized; any safely typed candidate text has already been deleted. */
|
|
35
52
|
onStart(): void;
|
|
36
|
-
/** The held
|
|
53
|
+
/** The held key was released (an idle gap with no further repeats, or any other key). */
|
|
37
54
|
onEnd(): void;
|
|
38
55
|
}
|
|
39
56
|
|
|
40
57
|
/** What the host must do with a keypress after {@link SpaceHoldGesture.process} has seen it:
|
|
41
|
-
* `"pass"` — handle it normally; `"type"` — insert
|
|
42
|
-
* turns into a hold); `"swallow"` — drop it, the gesture consumed it. */
|
|
58
|
+
* `"pass"` — handle it normally; `"type"` — insert its safely decoded printable text now (it is
|
|
59
|
+
* tracked back out if the run turns into a hold); `"swallow"` — drop it, the gesture consumed it. */
|
|
43
60
|
export type SpaceHoldStep = "pass" | "type" | "swallow";
|
|
44
61
|
|
|
45
|
-
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* taps are too slow, so neither escalates and both keep typing real spaces; the few spaces typed
|
|
50
|
-
* before a real hold is recognized are tracked back out of the host input. */
|
|
62
|
+
/** Push-to-talk key-hold state machine. A held key emits OS auto-repeat at a steady fast interval.
|
|
63
|
+
* We watch inter-key deltas and only recognize a hold once {@link SPACE_HOLD_MECHANICAL_RUN}
|
|
64
|
+
* consecutive deltas are mechanical (see {@link gapsAreMechanical}). Deliberate taps and jittery
|
|
65
|
+
* runs stay typed; only text typed during the current steady run is removed on recognition. */
|
|
51
66
|
export class SpaceHoldGesture {
|
|
52
|
-
/**
|
|
67
|
+
/** Canonical aliases of {@link keys}, rebuilt only when the bindings change (not per keystroke). */
|
|
68
|
+
#keyAliases = new Set<string>(["space"]);
|
|
69
|
+
#keys: readonly KeyId[] = ["space"];
|
|
70
|
+
/** Configured alternative keys. Alternatives are tracked independently and never combine into one run. */
|
|
71
|
+
get keys(): readonly KeyId[] {
|
|
72
|
+
return this.#keys;
|
|
73
|
+
}
|
|
74
|
+
set keys(keys: readonly KeyId[]) {
|
|
75
|
+
this.#keys = keys;
|
|
76
|
+
const aliases = new Set<string>();
|
|
77
|
+
for (const key of keys) addKeyAliases(aliases, key);
|
|
78
|
+
this.#keyAliases = aliases;
|
|
79
|
+
}
|
|
80
|
+
/** Unset (the default) keeps configured keys behaving normally. */
|
|
53
81
|
handler: SpaceHoldHandler | undefined;
|
|
54
82
|
/** Deletes the given number of characters before the host input's cursor. */
|
|
55
83
|
readonly #deleteTyped: (count: number) => void;
|
|
56
|
-
/** Host input state that rules the gesture out, e.g. an editor mode where
|
|
84
|
+
/** Host input state that rules the gesture out, e.g. an editor mode where keys are not text. */
|
|
57
85
|
readonly #allowed: () => boolean;
|
|
58
|
-
/**
|
|
86
|
+
/** Safely inserted text units in the current mechanical run; removed if a hold is recognized. */
|
|
59
87
|
#typed = 0;
|
|
60
|
-
/**
|
|
88
|
+
/** Length of the most recent candidate event actually typed into the host. */
|
|
89
|
+
#lastTypedLength = 0;
|
|
90
|
+
/** Canonical key in the current run, preventing alternate bindings from combining. */
|
|
91
|
+
#runKey: string | undefined;
|
|
92
|
+
/** Consecutive mechanical (fast + steady) deltas; a sustained run confirms a held key. */
|
|
61
93
|
#mechanicalRun = 0;
|
|
62
|
-
/** Inter-
|
|
94
|
+
/** Inter-key gap (ms) of the previous pair, compared against the next to judge steadiness. */
|
|
63
95
|
#prevGap: number | undefined;
|
|
64
|
-
/** Monotonic timestamp (ms) of the last
|
|
65
|
-
#
|
|
66
|
-
/**
|
|
96
|
+
/** Monotonic timestamp (ms) of the last configured keypress. */
|
|
97
|
+
#lastKeyAt = Number.NEGATIVE_INFINITY;
|
|
98
|
+
/** Canonical key while a recognized push-to-talk recording is in progress. */
|
|
99
|
+
#activeKey: string | undefined;
|
|
100
|
+
/** True while a recognized key-hold push-to-talk recording is in progress. */
|
|
67
101
|
#active = false;
|
|
68
|
-
/** Idle timer that ends the hold once repeated
|
|
102
|
+
/** Idle timer that ends the hold once repeated keypresses stop arriving. */
|
|
69
103
|
#releaseTimer: NodeJS.Timeout | undefined;
|
|
70
104
|
|
|
71
105
|
constructor(deleteTyped: (count: number) => void, allowed: () => boolean = () => true) {
|
|
@@ -73,58 +107,127 @@ export class SpaceHoldGesture {
|
|
|
73
107
|
this.#allowed = allowed;
|
|
74
108
|
}
|
|
75
109
|
|
|
76
|
-
/**
|
|
77
|
-
|
|
110
|
+
/** Pure focused-owner preflight on raw input: whether the key must reach the host before
|
|
111
|
+
* TUI-wide input listeners. Only non-printable bindings are claimed — they would otherwise
|
|
112
|
+
* trigger app shortcuts mid-hold; printable candidates keep the normal listener path so
|
|
113
|
+
* raw-input observers (e.g. extensions) still see typed text, as they did before remapping. */
|
|
114
|
+
shouldRoute(data: string): boolean {
|
|
115
|
+
if (!(this.handler?.enabled() && this.#allowed())) return false;
|
|
116
|
+
const parsedKey = parseKey(data);
|
|
117
|
+
if (parsedKey === undefined) return false;
|
|
118
|
+
const key = canonicalKeyId(parsedKey);
|
|
119
|
+
return this.#keyAliases.has(key) && getSpaceHoldText(data, key) === undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Feed one canonical keypress and, when safely printable, its decoded text length. The host
|
|
123
|
+
* records the actual inserted length with {@link recordTyped} after applying normal typing. */
|
|
124
|
+
process(key: string | undefined, insertedCharacterLength = 0): SpaceHoldStep {
|
|
125
|
+
const handler = this.handler;
|
|
126
|
+
if (!(handler?.enabled() && this.#allowed())) {
|
|
127
|
+
if (this.#active) this.#end();
|
|
128
|
+
else this.#resetRun();
|
|
129
|
+
return "pass";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const candidateKey = key !== undefined && this.#keyAliases.has(key) ? key : undefined;
|
|
133
|
+
|
|
78
134
|
if (this.#active) {
|
|
79
|
-
if (
|
|
80
|
-
// Auto-repeat while
|
|
135
|
+
if (candidateKey === this.#activeKey) {
|
|
136
|
+
// Auto-repeat while the same configured key is held: keep the release timer alive.
|
|
81
137
|
this.#armReleaseTimer();
|
|
82
138
|
return "swallow";
|
|
83
139
|
}
|
|
84
|
-
// Any
|
|
140
|
+
// Any other key means the active key was released. End this recording, then treat a
|
|
141
|
+
// different configured alternative as a fresh gesture rather than merging the keys.
|
|
85
142
|
this.#end();
|
|
86
|
-
return "pass";
|
|
87
143
|
}
|
|
88
|
-
|
|
144
|
+
|
|
145
|
+
if (candidateKey === undefined) {
|
|
89
146
|
this.#resetRun();
|
|
90
147
|
return "pass";
|
|
91
148
|
}
|
|
92
|
-
|
|
149
|
+
|
|
150
|
+
if (this.#runKey !== undefined && candidateKey !== this.#runKey) this.#resetRun();
|
|
93
151
|
const now = performance.now();
|
|
94
|
-
const
|
|
152
|
+
const hasPreviousKey = this.#lastKeyAt !== Number.NEGATIVE_INFINITY;
|
|
153
|
+
const gap = now - this.#lastKeyAt;
|
|
95
154
|
const prevGap = this.#prevGap;
|
|
96
|
-
this.#
|
|
155
|
+
this.#lastKeyAt = now;
|
|
156
|
+
this.#runKey = candidateKey;
|
|
157
|
+
const printable = insertedCharacterLength > 0;
|
|
158
|
+
|
|
159
|
+
if (!hasPreviousKey) {
|
|
160
|
+
this.#mechanicalRun = 0;
|
|
161
|
+
this.#typed = 0;
|
|
162
|
+
this.#lastTypedLength = 0;
|
|
163
|
+
return printable ? "type" : "swallow";
|
|
164
|
+
}
|
|
165
|
+
|
|
97
166
|
this.#prevGap = gap;
|
|
98
|
-
if (prevGap === undefined
|
|
99
|
-
//
|
|
100
|
-
//
|
|
167
|
+
if (prevGap === undefined) {
|
|
168
|
+
// The second event establishes the first interval. A fast gap keeps earlier literal
|
|
169
|
+
// typing eligible for rollback; a slow gap makes that first tap final.
|
|
170
|
+
this.#mechanicalRun = 0;
|
|
171
|
+
if (gap > SPACE_REPEAT_MAX_GAP_MS) {
|
|
172
|
+
this.#typed = 0;
|
|
173
|
+
this.#lastTypedLength = 0;
|
|
174
|
+
}
|
|
175
|
+
return printable ? "type" : "swallow";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!gapsAreMechanical(gap, prevGap)) {
|
|
179
|
+
// A fast interval after a slow gap may roll back the previous event and this one.
|
|
180
|
+
// Slow current gaps and jitter breaks keep prior text final.
|
|
101
181
|
this.#mechanicalRun = 0;
|
|
102
|
-
|
|
103
|
-
|
|
182
|
+
if (gap <= SPACE_REPEAT_MAX_GAP_MS && prevGap > SPACE_REPEAT_MAX_GAP_MS) {
|
|
183
|
+
this.#typed = this.#lastTypedLength;
|
|
184
|
+
} else {
|
|
185
|
+
this.#typed = 0;
|
|
186
|
+
this.#lastTypedLength = 0;
|
|
187
|
+
}
|
|
188
|
+
return printable ? "type" : "swallow";
|
|
104
189
|
}
|
|
105
|
-
|
|
106
|
-
//
|
|
190
|
+
|
|
191
|
+
// Steady fast repeat: swallow it. Once the cadence confirms a held key, remove only the
|
|
192
|
+
// literal candidate text from the current uninterrupted run and start recording.
|
|
107
193
|
if (++this.#mechanicalRun >= SPACE_HOLD_MECHANICAL_RUN) {
|
|
108
194
|
this.#deleteTyped(this.#typed);
|
|
109
195
|
this.#resetRun();
|
|
110
196
|
this.#active = true;
|
|
197
|
+
this.#activeKey = candidateKey;
|
|
111
198
|
this.#armReleaseTimer();
|
|
112
|
-
|
|
199
|
+
handler.onStart();
|
|
113
200
|
}
|
|
114
201
|
return "swallow";
|
|
115
202
|
}
|
|
116
203
|
|
|
204
|
+
/** Account for text the host actually inserted for the preceding `"type"` step. */
|
|
205
|
+
recordTyped(insertedCharacterLength: number): void {
|
|
206
|
+
const insertedLength = Math.max(0, insertedCharacterLength);
|
|
207
|
+
this.#typed += insertedLength;
|
|
208
|
+
this.#lastTypedLength = insertedLength;
|
|
209
|
+
}
|
|
210
|
+
|
|
117
211
|
#resetRun(): void {
|
|
212
|
+
this.#lastTypedLength = 0;
|
|
118
213
|
this.#typed = 0;
|
|
214
|
+
this.#runKey = undefined;
|
|
119
215
|
this.#mechanicalRun = 0;
|
|
120
216
|
this.#prevGap = undefined;
|
|
121
|
-
this.#
|
|
217
|
+
this.#lastKeyAt = Number.NEGATIVE_INFINITY;
|
|
122
218
|
}
|
|
123
219
|
|
|
124
220
|
#armReleaseTimer(): void {
|
|
125
|
-
|
|
221
|
+
clearTimeout(this.#releaseTimer);
|
|
222
|
+
const armedAt = performance.now();
|
|
126
223
|
this.#releaseTimer = setTimeout(() => {
|
|
127
224
|
this.#releaseTimer = undefined;
|
|
225
|
+
// Late firing means input went unobserved; wait one more window so queued repeats can
|
|
226
|
+
// re-arm the timer before deciding the key was released.
|
|
227
|
+
if (performance.now() - armedAt > SPACE_HOLD_RELEASE_MS + SPACE_HOLD_STALL_SLACK_MS) {
|
|
228
|
+
this.#armReleaseTimer();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
128
231
|
this.#end();
|
|
129
232
|
}, SPACE_HOLD_RELEASE_MS);
|
|
130
233
|
this.#releaseTimer.unref?.();
|
|
@@ -133,6 +236,7 @@ export class SpaceHoldGesture {
|
|
|
133
236
|
#end(): void {
|
|
134
237
|
if (!this.#active) return;
|
|
135
238
|
this.#active = false;
|
|
239
|
+
this.#activeKey = undefined;
|
|
136
240
|
this.#resetRun();
|
|
137
241
|
if (this.#releaseTimer) {
|
|
138
242
|
clearTimeout(this.#releaseTimer);
|