@oh-my-pi/pi-tui 18.4.3 → 18.4.4
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 +36 -0
- package/dist/types/apps/cleanse-board.d.ts +17 -0
- package/dist/types/apps/git/help.d.ts +16 -0
- package/dist/types/apps/git/sidebar.d.ts +18 -0
- package/dist/types/apps/live-visualizer.d.ts +10 -0
- package/dist/types/apps/ps-top.d.ts +15 -0
- package/dist/types/apps/standalone-picker.d.ts +3 -0
- package/dist/types/autocomplete.d.ts +8 -0
- package/dist/types/chat/advisor-message.d.ts +12 -2
- package/dist/types/chat/assistant-message.d.ts +20 -0
- package/dist/types/chat/bash-execution.d.ts +9 -0
- package/dist/types/chat/chat-transcript-builder.d.ts +6 -1
- package/dist/types/chat/collab-prompt-message.d.ts +4 -0
- package/dist/types/chat/compaction-summary-message.d.ts +15 -0
- package/dist/types/chat/eval-execution.d.ts +8 -0
- package/dist/types/chat/execution-shared.d.ts +58 -0
- package/dist/types/chat/late-diagnostics-message.d.ts +15 -1
- package/dist/types/chat/read-tool-group.d.ts +15 -0
- package/dist/types/chat/skill-message.d.ts +9 -0
- package/dist/types/chat/stripped-tool-calls-placeholder.d.ts +3 -0
- package/dist/types/chat/todo-reminder.d.ts +4 -1
- package/dist/types/chat/tool-execution.d.ts +25 -1
- package/dist/types/chat/transcript-actions.d.ts +37 -0
- package/dist/types/chat/transcript-entry.d.ts +3 -1
- package/dist/types/chat/user-message.d.ts +25 -0
- package/dist/types/chrome/collab-qrcode.d.ts +7 -0
- package/dist/types/chrome/context-thresholds.d.ts +11 -0
- package/dist/types/chrome/countdown-timer.d.ts +9 -3
- package/dist/types/chrome/diff.d.ts +12 -0
- package/dist/types/chrome/dynamic-border.d.ts +3 -0
- package/dist/types/chrome/format.d.ts +6 -0
- package/dist/types/chrome/message-divider.d.ts +13 -0
- package/dist/types/chrome/message-frame.d.ts +10 -0
- package/dist/types/chrome/message-notice.d.ts +23 -0
- package/dist/types/chrome/overlay-box.d.ts +11 -2
- package/dist/types/chrome/qrcode.d.ts +7 -0
- package/dist/types/chrome/segment-track.d.ts +7 -0
- package/dist/types/chrome/selector-helpers.d.ts +0 -6
- package/dist/types/chrome/status-notice.d.ts +14 -0
- package/dist/types/chrome/streaming-panel.d.ts +3 -0
- package/dist/types/chrome/tool-activity.d.ts +3 -0
- package/dist/types/chrome/transcript-container.d.ts +13 -0
- package/dist/types/chrome/transcript-status.d.ts +15 -3
- package/dist/types/components/box.d.ts +7 -0
- package/dist/types/components/disclosure.d.ts +9 -0
- package/dist/types/components/editor.d.ts +37 -0
- package/dist/types/components/form.d.ts +14 -0
- package/dist/types/components/image.d.ts +7 -0
- package/dist/types/components/input.d.ts +20 -1
- package/dist/types/components/key-value-list.d.ts +3 -0
- package/dist/types/components/layout/geometry.d.ts +16 -0
- package/dist/types/components/layout/row.d.ts +7 -0
- package/dist/types/components/layout/split-pane.d.ts +8 -0
- package/dist/types/components/layout/stack.d.ts +6 -0
- package/dist/types/components/loader.d.ts +48 -0
- package/dist/types/components/markdown.d.ts +8 -0
- package/dist/types/components/metric.d.ts +7 -0
- package/dist/types/components/progress-bar.d.ts +3 -0
- package/dist/types/components/scroll-view.d.ts +6 -0
- package/dist/types/components/section.d.ts +3 -0
- package/dist/types/components/select-list.d.ts +33 -0
- package/dist/types/components/settings-list.d.ts +68 -0
- package/dist/types/components/spacer.d.ts +4 -0
- package/dist/types/components/tab-bar.d.ts +9 -0
- package/dist/types/components/table.d.ts +7 -0
- package/dist/types/components/text.d.ts +6 -0
- package/dist/types/components/tree-view.d.ts +9 -0
- package/dist/types/components/truncated-text.d.ts +3 -0
- package/dist/types/components/wizard-step.d.ts +6 -0
- package/dist/types/hotkeys-markdown.d.ts +24 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/latex-block.d.ts +7 -0
- package/dist/types/native/apply.d.ts +39 -0
- package/dist/types/native/backend.d.ts +94 -0
- package/dist/types/native/blobs.d.ts +29 -0
- package/dist/types/native/describe.d.ts +56 -0
- package/dist/types/native/encode.d.ts +50 -0
- package/dist/types/native/icons.d.ts +18 -0
- package/dist/types/native/memo.d.ts +27 -0
- package/dist/types/native/node.d.ts +107 -0
- package/dist/types/native/overlay.d.ts +64 -0
- package/dist/types/native/picker.d.ts +124 -0
- package/dist/types/native/reconcile.d.ts +41 -0
- package/dist/types/native/settle.d.ts +11 -0
- package/dist/types/native/spans.d.ts +21 -0
- package/dist/types/native/state.d.ts +6 -0
- package/dist/types/native/tone.d.ts +24 -0
- package/dist/types/overlays/advisor-config.d.ts +20 -0
- package/dist/types/overlays/agent-hub-renderer.d.ts +27 -11
- package/dist/types/overlays/agent-hub.d.ts +10 -0
- package/dist/types/overlays/agent-transcript-viewer.d.ts +7 -0
- package/dist/types/overlays/agents-hub.d.ts +8 -0
- package/dist/types/overlays/annotation-overlay.d.ts +3 -0
- package/dist/types/overlays/ask-dialog.d.ts +23 -0
- package/dist/types/overlays/bordered-loader.d.ts +3 -0
- package/dist/types/overlays/btw-history-panel.d.ts +9 -0
- package/dist/types/overlays/btw-panel.d.ts +3 -0
- package/dist/types/overlays/cleanse-panel.d.ts +5 -2
- package/dist/types/overlays/composer-shape-preview.d.ts +7 -1
- package/dist/types/overlays/copy-selector.d.ts +38 -0
- package/dist/types/overlays/error-banner.d.ts +9 -0
- package/dist/types/overlays/extensions/extension-list.d.ts +1 -0
- package/dist/types/overlays/history-search.d.ts +18 -0
- package/dist/types/overlays/hook-editor.d.ts +28 -1
- package/dist/types/overlays/hook-input.d.ts +3 -0
- package/dist/types/overlays/hook-selector.d.ts +10 -0
- package/dist/types/overlays/hub-frame.d.ts +18 -0
- package/dist/types/overlays/jobs-panel.d.ts +24 -0
- package/dist/types/overlays/login-dialog.d.ts +10 -0
- package/dist/types/overlays/logout-account-selector.d.ts +4 -0
- package/dist/types/overlays/mcp-add-wizard.d.ts +4 -0
- package/dist/types/overlays/model-browser.d.ts +69 -0
- package/dist/types/overlays/model-hub.d.ts +7 -0
- package/dist/types/overlays/model-picker.d.ts +7 -0
- package/dist/types/overlays/move-overlay.d.ts +3 -0
- package/dist/types/overlays/oauth-selector.d.ts +4 -0
- package/dist/types/overlays/omfg-panel.d.ts +3 -0
- package/dist/types/overlays/pause-screen.d.ts +12 -0
- package/dist/types/overlays/plan-review-overlay.d.ts +5 -0
- package/dist/types/overlays/plan-save-overlay.d.ts +10 -0
- package/dist/types/overlays/plugin-selector.d.ts +4 -0
- package/dist/types/overlays/plugin-settings.d.ts +30 -0
- package/dist/types/overlays/queue-mode-selector.d.ts +4 -0
- package/dist/types/overlays/reset-usage-selector.d.ts +8 -0
- package/dist/types/overlays/rewind-selector.d.ts +5 -0
- package/dist/types/overlays/session-info-overlay.d.ts +19 -1
- package/dist/types/overlays/session-selector.d.ts +64 -1
- package/dist/types/overlays/settings-defs.d.ts +14 -0
- package/dist/types/overlays/settings-selector.d.ts +21 -0
- package/dist/types/overlays/show-images-selector.d.ts +4 -0
- package/dist/types/overlays/snapcompact-shape-preview.d.ts +2 -0
- package/dist/types/overlays/stats-notice.d.ts +8 -0
- package/dist/types/overlays/theme-selector.d.ts +11 -0
- package/dist/types/overlays/thinking-selector.d.ts +4 -0
- package/dist/types/overlays/tree-selector.d.ts +60 -2
- package/dist/types/overlays/usage-dashboard.d.ts +26 -3
- package/dist/types/overlays/usage-row.d.ts +36 -2
- package/dist/types/prompt/attachment-chips.d.ts +8 -0
- package/dist/types/prompt/composer-attachments.d.ts +15 -3
- package/dist/types/prompt/composer.d.ts +18 -1
- package/dist/types/prompt/custom-editor.d.ts +48 -2
- package/dist/types/prompt/editor-top-gap.d.ts +2 -0
- package/dist/types/prompt/macos-spelling.d.ts +6 -0
- package/dist/types/prompt/magic-keywords.d.ts +9 -0
- package/dist/types/prompt/queued-messages.d.ts +23 -0
- package/dist/types/prompt/skill-tokens.d.ts +4 -2
- package/dist/types/prompt/welcome.d.ts +16 -0
- package/dist/types/render/code-cell.d.ts +10 -0
- package/dist/types/render/file-list.d.ts +8 -0
- package/dist/types/render/hyperlink.d.ts +14 -0
- package/dist/types/render/output-block.d.ts +43 -0
- package/dist/types/render/output-pane.d.ts +17 -0
- package/dist/types/render/tool-card.d.ts +8 -0
- package/dist/types/render/tree-list.d.ts +8 -0
- package/dist/types/render/width-aware-text.d.ts +7 -0
- package/dist/types/setup/scenes/outro.d.ts +7 -0
- package/dist/types/setup/scenes/sign-in.d.ts +7 -0
- package/dist/types/setup/scenes/splash.d.ts +8 -0
- package/dist/types/setup/scenes/types.d.ts +6 -1
- package/dist/types/setup/wizard-overlay.d.ts +9 -0
- package/dist/types/status-line/component.d.ts +49 -5
- package/dist/types/status-line/context-usage.d.ts +20 -0
- package/dist/types/status-line/footer.d.ts +8 -0
- package/dist/types/status-line/metrics.d.ts +1 -2
- package/dist/types/status-line/segments.d.ts +10 -1
- package/dist/types/status-line/types.d.ts +40 -1
- package/dist/types/terminal-capabilities.d.ts +1 -1
- package/dist/types/terminal.d.ts +27 -0
- package/dist/types/theme/defaults/index.d.ts +100 -0
- package/dist/types/theme/schema.d.ts +2 -0
- package/dist/types/theme/shimmer.d.ts +8 -0
- package/dist/types/theme/theme-class.d.ts +2 -0
- package/dist/types/theme/theme.d.ts +33 -1
- package/dist/types/tools/ask.d.ts +3 -0
- package/dist/types/tools/ast-edit.d.ts +10 -1
- package/dist/types/tools/ast-grep.d.ts +10 -1
- package/dist/types/tools/autoresearch.d.ts +21 -4
- package/dist/types/tools/bash.d.ts +30 -1
- package/dist/types/tools/cfg-render.d.ts +5 -1
- package/dist/types/tools/debug.d.ts +3 -1
- package/dist/types/tools/default-renderer.d.ts +6 -1
- package/dist/types/tools/edit.d.ts +14 -1
- package/dist/types/tools/eval.d.ts +12 -1
- package/dist/types/tools/fetch.d.ts +16 -1
- package/dist/types/tools/find.d.ts +15 -1
- package/dist/types/tools/github.d.ts +3 -0
- package/dist/types/tools/glob.d.ts +14 -1
- package/dist/types/tools/goal.d.ts +3 -1
- package/dist/types/tools/grep.d.ts +14 -1
- package/dist/types/tools/grouped-file-output.d.ts +58 -0
- package/dist/types/tools/json-tree.d.ts +20 -0
- package/dist/types/tools/lsp.d.ts +3 -10
- package/dist/types/tools/mcp.d.ts +16 -1
- package/dist/types/tools/memory.d.ts +26 -0
- package/dist/types/tools/native-view.d.ts +90 -0
- package/dist/types/tools/proc-render.d.ts +7 -1
- package/dist/types/tools/read.d.ts +23 -1
- package/dist/types/tools/renderer.d.ts +57 -0
- package/dist/types/tools/report-tool-issue.d.ts +3 -0
- package/dist/types/tools/resolve.d.ts +14 -1
- package/dist/types/tools/task.d.ts +3 -0
- package/dist/types/tools/think.d.ts +3 -0
- package/dist/types/tools/todo.d.ts +12 -0
- package/dist/types/tools/vibe.d.ts +3 -1
- package/dist/types/tools/wait.d.ts +3 -0
- package/dist/types/tools/web-search.d.ts +8 -0
- package/dist/types/tools/write.d.ts +14 -1
- package/dist/types/tools/xdev.d.ts +13 -0
- package/dist/types/tui.d.ts +49 -0
- package/package.json +9 -9
- package/src/apps/autoresearch-dashboard.ts +362 -4
- package/src/apps/cleanse-board.ts +219 -1
- package/src/apps/cleanse-picker.ts +9 -1
- package/src/apps/debug/log-viewer.ts +15 -15
- package/src/apps/debug/protocol-probe.ts +2 -20
- package/src/apps/git/avatar.ts +2 -22
- package/src/apps/git/git-tui.ts +406 -59
- package/src/apps/git/help.ts +130 -0
- package/src/apps/git/sidebar.ts +491 -0
- package/src/apps/live-visualizer.ts +65 -0
- package/src/apps/ps-top.ts +448 -17
- package/src/apps/session-picker.ts +1 -0
- package/src/apps/setup-model-picker.ts +5 -1
- package/src/apps/standalone-picker.ts +55 -1
- package/src/autocomplete.ts +44 -1
- package/src/chat/advisor-message.ts +72 -3
- package/src/chat/assistant-message.ts +346 -11
- package/src/chat/background-tan-message.ts +4 -4
- package/src/chat/bash-execution.ts +81 -0
- package/src/chat/cache-invalidation-marker.ts +8 -0
- package/src/chat/chat-transcript-builder.ts +37 -6
- package/src/chat/collab-prompt-message.ts +15 -3
- package/src/chat/compaction-summary-message.ts +74 -1
- package/src/chat/custom-message.ts +3 -1
- package/src/chat/eval-execution.ts +57 -0
- package/src/chat/execution-shared.ts +153 -1
- package/src/chat/hook-message.ts +1 -0
- package/src/chat/late-diagnostics-message.ts +85 -2
- package/src/chat/read-tool-group.ts +341 -1
- package/src/chat/served-model-marker.ts +8 -0
- package/src/chat/skill-message.ts +63 -0
- package/src/chat/stripped-tool-calls-placeholder.ts +18 -10
- package/src/chat/todo-reminder.ts +34 -4
- package/src/chat/tool-execution.ts +385 -10
- package/src/chat/transcript-actions.ts +38 -0
- package/src/chat/transcript-browser.ts +2 -9
- package/src/chat/transcript-entry.ts +10 -1
- package/src/chat/transcript-render-helpers.ts +16 -15
- package/src/chat/ttsr-notification.ts +38 -1
- package/src/chat/user-message.ts +167 -4
- package/src/chrome/collab-qrcode.ts +27 -2
- package/src/chrome/context-thresholds.ts +34 -0
- package/src/chrome/countdown-timer.ts +24 -1
- package/src/chrome/diff.ts +60 -1
- package/src/chrome/dynamic-border.ts +7 -0
- package/src/chrome/format.ts +12 -0
- package/src/chrome/message-divider.ts +33 -1
- package/src/chrome/message-frame.ts +53 -10
- package/src/chrome/message-notice.ts +71 -0
- package/src/chrome/overlay-box.ts +45 -2
- package/src/chrome/qrcode.ts +28 -0
- package/src/chrome/segment-track.ts +26 -0
- package/src/chrome/selector-helpers.ts +1 -17
- package/src/chrome/status-notice.ts +46 -0
- package/src/chrome/streaming-panel.ts +25 -1
- package/src/chrome/tool-activity.ts +20 -0
- package/src/chrome/transcript-container.ts +37 -0
- package/src/chrome/transcript-status.ts +40 -11
- package/src/components/box.ts +51 -0
- package/src/components/disclosure.ts +33 -0
- package/src/components/editor.ts +216 -27
- package/src/components/form.ts +93 -9
- package/src/components/image.ts +30 -0
- package/src/components/input.ts +77 -26
- package/src/components/key-value-list.ts +18 -0
- package/src/components/layout/geometry.ts +44 -0
- package/src/components/layout/row.ts +30 -0
- package/src/components/layout/split-pane.ts +65 -0
- package/src/components/layout/stack.ts +27 -0
- package/src/components/loader.ts +183 -1
- package/src/components/markdown.ts +21 -0
- package/src/components/metric.ts +39 -0
- package/src/components/progress-bar.ts +28 -0
- package/src/components/scroll-view.ts +23 -0
- package/src/components/section.ts +16 -0
- package/src/components/select-list.ts +174 -25
- package/src/components/settings-list.ts +264 -7
- package/src/components/spacer.ts +17 -0
- package/src/components/tab-bar.ts +42 -0
- package/src/components/table.ts +34 -0
- package/src/components/text.ts +23 -0
- package/src/components/tree-view.ts +65 -0
- package/src/components/truncated-text.ts +14 -0
- package/src/components/wizard-step.ts +18 -9
- package/src/debug-server.ts +14 -7
- package/src/hotkeys-markdown.ts +251 -69
- package/src/index.ts +6 -0
- package/src/latex-block.ts +11 -0
- package/src/native/apply.ts +275 -0
- package/src/native/backend.ts +682 -0
- package/src/native/blobs.ts +70 -0
- package/src/native/describe.ts +124 -0
- package/src/native/encode.ts +233 -0
- package/src/native/icons.ts +184 -0
- package/src/native/memo.ts +64 -0
- package/src/native/node.ts +112 -0
- package/src/native/overlay.ts +131 -0
- package/src/native/picker.ts +333 -0
- package/src/native/reconcile.ts +1063 -0
- package/src/native/settle.ts +23 -0
- package/src/native/spans.ts +308 -0
- package/src/native/state.ts +28 -0
- package/src/native/tone.ts +79 -0
- package/src/overlays/advisor-config.ts +420 -0
- package/src/overlays/agent-hub-renderer.ts +155 -18
- package/src/overlays/agent-hub.ts +1037 -44
- package/src/overlays/agent-transcript-viewer.ts +129 -0
- package/src/overlays/agents-hub.ts +688 -53
- package/src/overlays/annotation-overlay.ts +391 -52
- package/src/overlays/ask-dialog.ts +435 -24
- package/src/overlays/bordered-loader.ts +17 -0
- package/src/overlays/btw-history-panel.ts +182 -0
- package/src/overlays/btw-panel.ts +68 -2
- package/src/overlays/cleanse-panel.ts +105 -15
- package/src/overlays/codex-reset-fireworks.ts +65 -5
- package/src/overlays/composer-shape-preview.ts +26 -1
- package/src/overlays/copy-selector.ts +545 -23
- package/src/overlays/error-banner.ts +44 -0
- package/src/overlays/extensions/extension-list.ts +23 -17
- package/src/overlays/history-search.ts +175 -33
- package/src/overlays/hook-editor.ts +113 -6
- package/src/overlays/hook-input.ts +22 -1
- package/src/overlays/hook-selector.ts +311 -22
- package/src/overlays/hub-frame.ts +80 -0
- package/src/overlays/jobs-panel.ts +112 -0
- package/src/overlays/login-dialog.ts +183 -1
- package/src/overlays/logout-account-selector.ts +102 -4
- package/src/overlays/mcp-add-wizard.ts +232 -53
- package/src/overlays/model-browser.ts +681 -22
- package/src/overlays/model-hub.ts +1402 -78
- package/src/overlays/model-picker.ts +175 -1
- package/src/overlays/move-overlay.ts +45 -0
- package/src/overlays/oauth-selector.ts +227 -23
- package/src/overlays/omfg-panel.ts +86 -2
- package/src/overlays/pause-screen.ts +55 -4
- package/src/overlays/plan-review-overlay.ts +352 -0
- package/src/overlays/plan-save-overlay.ts +45 -0
- package/src/overlays/plugin-selector.ts +21 -1
- package/src/overlays/plugin-settings.ts +195 -5
- package/src/overlays/queue-mode-selector.ts +19 -1
- package/src/overlays/reset-usage-selector.ts +110 -40
- package/src/overlays/rewind-selector.ts +458 -44
- package/src/overlays/session-account-selector.ts +1 -1
- package/src/overlays/session-info-overlay.ts +137 -1
- package/src/overlays/session-selector.ts +807 -46
- package/src/overlays/settings-defs.ts +41 -0
- package/src/overlays/settings-selector.ts +583 -23
- package/src/overlays/show-images-selector.ts +18 -1
- package/src/overlays/snapcompact-shape-preview.ts +48 -3
- package/src/overlays/stats-notice.ts +31 -0
- package/src/overlays/theme-selector.ts +31 -1
- package/src/overlays/thinking-selector.ts +22 -1
- package/src/overlays/tree-selector.ts +570 -110
- package/src/overlays/usage-dashboard.ts +587 -12
- package/src/overlays/usage-row.ts +161 -25
- package/src/prompt/attachment-chips.ts +60 -0
- package/src/prompt/composer-attachments.ts +31 -9
- package/src/prompt/composer.ts +55 -2
- package/src/prompt/custom-editor.ts +302 -4
- package/src/prompt/editor-top-gap.ts +8 -0
- package/src/prompt/macos-spelling.ts +24 -9
- package/src/prompt/magic-keywords.ts +22 -1
- package/src/prompt/model-mention-autocomplete.ts +1 -0
- package/src/prompt/queued-messages.ts +101 -0
- package/src/prompt/skill-tokens.ts +5 -3
- package/src/prompt/welcome.ts +213 -6
- package/src/render/code-cell.ts +103 -2
- package/src/render/file-list.ts +29 -0
- package/src/render/hyperlink.ts +32 -0
- package/src/render/output-block.ts +110 -0
- package/src/render/output-pane.ts +37 -0
- package/src/render/tool-card.ts +137 -0
- package/src/render/tree-list.ts +39 -0
- package/src/render/width-aware-text.ts +20 -0
- package/src/setup/scenes/composer.ts +29 -0
- package/src/setup/scenes/glyph.ts +15 -0
- package/src/setup/scenes/model.ts +36 -8
- package/src/setup/scenes/outro.ts +28 -1
- package/src/setup/scenes/sign-in.ts +122 -13
- package/src/setup/scenes/splash.ts +28 -1
- package/src/setup/scenes/theme.ts +88 -3
- package/src/setup/scenes/types.ts +7 -1
- package/src/setup/startup-splash.ts +46 -9
- package/src/setup/wizard-overlay.ts +140 -23
- package/src/status-line/component.ts +398 -16
- package/src/status-line/context-usage.ts +279 -50
- package/src/status-line/footer.ts +94 -1
- package/src/status-line/metrics.ts +3 -6
- package/src/status-line/segments.ts +463 -97
- package/src/status-line/separators.ts +3 -16
- package/src/status-line/types.ts +43 -1
- package/src/terminal-capabilities.ts +7 -0
- package/src/terminal.ts +130 -2
- package/src/theme/defaults/dark-neon-noir.json +100 -0
- package/src/theme/defaults/index.ts +2 -0
- package/src/theme/defaults/light-prism.json +5 -5
- package/src/theme/defaults/onyx.json +62 -62
- package/src/theme/schema.ts +15 -0
- package/src/theme/shimmer.ts +33 -0
- package/src/theme/theme-class.ts +24 -19
- package/src/theme/theme.ts +111 -9
- package/src/tools/ask.ts +157 -1
- package/src/tools/ast-edit.ts +174 -1
- package/src/tools/ast-grep.ts +82 -2
- package/src/tools/autoresearch.ts +149 -1
- package/src/tools/bash.ts +180 -49
- package/src/tools/cfg-render.ts +66 -1
- package/src/tools/debug.ts +78 -1
- package/src/tools/default-renderer.ts +35 -1
- package/src/tools/edit.ts +247 -2
- package/src/tools/eval.ts +257 -1
- package/src/tools/fetch.ts +102 -3
- package/src/tools/find.ts +127 -1
- package/src/tools/github.ts +119 -10
- package/src/tools/glob.ts +116 -1
- package/src/tools/goal.ts +56 -1
- package/src/tools/grep.ts +83 -2
- package/src/tools/grouped-file-output.ts +205 -0
- package/src/tools/json-tree.ts +116 -1
- package/src/tools/lsp.ts +318 -1
- package/src/tools/mcp.ts +80 -1
- package/src/tools/memory.ts +93 -11
- package/src/tools/native-view.ts +292 -0
- package/src/tools/proc-render.ts +221 -1
- package/src/tools/read.ts +199 -4
- package/src/tools/renderer.ts +61 -0
- package/src/tools/report-tool-issue.ts +7 -1
- package/src/tools/resolve.ts +63 -1
- package/src/tools/task.ts +406 -4
- package/src/tools/think.ts +19 -1
- package/src/tools/todo.ts +89 -0
- package/src/tools/vibe.ts +230 -2
- package/src/tools/wait.ts +181 -1
- package/src/tools/web-search.ts +113 -1
- package/src/tools/write.ts +144 -5
- package/src/tools/xdev.ts +64 -1
- package/src/tui.ts +294 -3
|
@@ -0,0 +1,1063 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconciler: turns successive component descriptions into minimal TSP ops.
|
|
3
|
+
*
|
|
4
|
+
* Ids. Every component instance gets a stable base-36 id (its root node's
|
|
5
|
+
* wire id); a node nested inside it gets `<base>.<keypath>`, where the
|
|
6
|
+
* keypath joins each level's `key` (or child index) with `/`. Ids therefore
|
|
7
|
+
* derive from the description itself, so the reconciler keeps no per-node
|
|
8
|
+
* table: its whole memory is, per mounted component, the node object the
|
|
9
|
+
* component last returned (which the component itself retains as its memo),
|
|
10
|
+
* the component boundaries and hoisted overlays found inside it, and
|
|
11
|
+
* rows-fallback bookkeeping.
|
|
12
|
+
*
|
|
13
|
+
* Diffing. Reference-equal nodes are skipped without walking; component
|
|
14
|
+
* boundaries inside them are still visited through the recorded boundary
|
|
15
|
+
* list. Children are matched by id; keyed reorders become `move`s placed
|
|
16
|
+
* around the longest run of children that kept their order, removals are
|
|
17
|
+
* deferred to the end of the frame so a component moved elsewhere in the same
|
|
18
|
+
* frame keeps its terminal-side state. Primary text that extends the previous
|
|
19
|
+
* text becomes `text append`.
|
|
20
|
+
*
|
|
21
|
+
* Settling. A settled component (see `settle.ts`) drops its retained node
|
|
22
|
+
* after the `settle` hint; only a weak identity, its root kind, root prop
|
|
23
|
+
* names and child ids survive. A later change is sent as targeted ops by id
|
|
24
|
+
* (`set` of the root props, `del` + `add` of its children), never by diffing
|
|
25
|
+
* against state omp no longer keeps.
|
|
26
|
+
*
|
|
27
|
+
* `overlay` nodes described anywhere but directly under `layer` are hoisted
|
|
28
|
+
* into `layer`; their anchor keypaths are rewritten to wire ids.
|
|
29
|
+
*/
|
|
30
|
+
import * as logger from "@oh-my-pi/pi-utils/logger";
|
|
31
|
+
import { TSP_TEXT_KINDS, type TspKind, type TspNode, type TspOp } from "@oh-my-pi/pi-wire";
|
|
32
|
+
import { type Component, Container, CURSOR_MARKER } from "../tui";
|
|
33
|
+
import { normalizeIconProps } from "./icons";
|
|
34
|
+
import type { DescribeContext, NativeChild, NativeNode } from "./node";
|
|
35
|
+
import { isNativeSettled } from "./settle";
|
|
36
|
+
|
|
37
|
+
/** The regions a frame fills. */
|
|
38
|
+
export interface NativeRegions {
|
|
39
|
+
readonly main: readonly NativeChild[];
|
|
40
|
+
readonly dock: readonly NativeChild[];
|
|
41
|
+
readonly layer: readonly NativeChild[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Where a wire id came from, for event routing. */
|
|
45
|
+
export interface NativeTarget {
|
|
46
|
+
readonly component: Component;
|
|
47
|
+
/** Keypath inside the component ("" for its root node). */
|
|
48
|
+
readonly keypath: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const kNativeId = Symbol("native.id");
|
|
52
|
+
|
|
53
|
+
interface IdTagged {
|
|
54
|
+
[kNativeId]?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const REGION_IDS = ["main", "dock", "layer"] as const;
|
|
58
|
+
type RegionId = (typeof REGION_IDS)[number];
|
|
59
|
+
const RESERVED_IDS: ReadonlySet<string> = new Set(REGION_IDS);
|
|
60
|
+
const TEXT_KINDS: ReadonlySet<string> = new Set(TSP_TEXT_KINDS);
|
|
61
|
+
let nextComponentId = 0;
|
|
62
|
+
|
|
63
|
+
/** Stable base-36 wire id of a component instance (its root node's id). */
|
|
64
|
+
export function nativeComponentId(component: Component): string {
|
|
65
|
+
const tagged = component as IdTagged;
|
|
66
|
+
let id = tagged[kNativeId];
|
|
67
|
+
if (id === undefined) {
|
|
68
|
+
do {
|
|
69
|
+
id = (nextComponentId++).toString(36);
|
|
70
|
+
} while (RESERVED_IDS.has(id));
|
|
71
|
+
tagged[kNativeId] = id;
|
|
72
|
+
}
|
|
73
|
+
return id;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isComponent(child: NativeChild): child is Component {
|
|
77
|
+
return typeof (child as Component).render === "function";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const KEY_ESCAPES: Readonly<Record<string, string>> = { "%": "%25", "/": "%2F", "^": "%5E" };
|
|
81
|
+
const KEY_UNESCAPES: Readonly<Record<string, string>> = { "%25": "%", "%2F": "/", "%5E": "^" };
|
|
82
|
+
|
|
83
|
+
function escapeKey(key: string): string {
|
|
84
|
+
return key.replace(/[%/^]/g, c => KEY_ESCAPES[c]!);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A keypath segment back to the key it was built from (hoisted overlays carry a `^` marker). */
|
|
88
|
+
function unescapeKey(segment: string): string {
|
|
89
|
+
return (segment.startsWith("^") ? segment.slice(1) : segment).replace(/%2F|%25|%5E/g, c => KEY_UNESCAPES[c]!);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function nodeId(base: string, keypath: string): string {
|
|
93
|
+
return keypath === "" ? base : `${base}.${keypath}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function sameValue(a: unknown, b: unknown): boolean {
|
|
97
|
+
if (a === b) return true;
|
|
98
|
+
return typeof a === "object" && typeof b === "object" && a !== null && b !== null && Bun.deepEquals(a, b);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function asProps(node: NativeNode): Readonly<Record<string, unknown>> | undefined {
|
|
102
|
+
return node.p as Readonly<Record<string, unknown>> | undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Longest increasing subsequence over non-negative `sources`; marks the members. */
|
|
106
|
+
function stableMask(sources: readonly number[]): boolean[] {
|
|
107
|
+
const n = sources.length;
|
|
108
|
+
const mask = Array.from({ length: n }, () => false);
|
|
109
|
+
const tails: number[] = [];
|
|
110
|
+
const prev = Array.from({ length: n }, () => -1);
|
|
111
|
+
for (let i = 0; i < n; i++) {
|
|
112
|
+
const value = sources[i]!;
|
|
113
|
+
if (value < 0) continue;
|
|
114
|
+
let lo = 0;
|
|
115
|
+
let hi = tails.length;
|
|
116
|
+
while (lo < hi) {
|
|
117
|
+
const mid = (lo + hi) >> 1;
|
|
118
|
+
if (sources[tails[mid]!]! < value) lo = mid + 1;
|
|
119
|
+
else hi = mid;
|
|
120
|
+
}
|
|
121
|
+
if (lo > 0) prev[i] = tails[lo - 1]!;
|
|
122
|
+
tails[lo] = i;
|
|
123
|
+
}
|
|
124
|
+
for (let at = tails.length > 0 ? tails[tails.length - 1]! : -1; at >= 0; at = prev[at]!) mask[at] = true;
|
|
125
|
+
return mask;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface Owner {
|
|
129
|
+
readonly base: string;
|
|
130
|
+
readonly state: CompState | null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface NodeEntry {
|
|
134
|
+
readonly id: string;
|
|
135
|
+
readonly node: NativeNode;
|
|
136
|
+
readonly owner: Owner;
|
|
137
|
+
readonly keypath: string;
|
|
138
|
+
/** Whether overlay descendants of this node are hoisted into `layer`. */
|
|
139
|
+
readonly hoist: boolean;
|
|
140
|
+
readonly comp?: undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface CompEntry {
|
|
144
|
+
readonly id: string;
|
|
145
|
+
readonly comp: Component;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
type Entry = NodeEntry | CompEntry;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* What a component's walk met, in walk order: nested component boundaries
|
|
152
|
+
* (with their document position) and hoisted overlays. Replaying it for an
|
|
153
|
+
* unchanged component reproduces the walk's visits and overlay order.
|
|
154
|
+
*/
|
|
155
|
+
type Inner =
|
|
156
|
+
| {
|
|
157
|
+
readonly comp: Component;
|
|
158
|
+
readonly parent: string;
|
|
159
|
+
readonly before: string | null;
|
|
160
|
+
readonly portal?: undefined;
|
|
161
|
+
}
|
|
162
|
+
| { readonly portal: NodeEntry; readonly comp?: undefined };
|
|
163
|
+
|
|
164
|
+
interface CompState {
|
|
165
|
+
readonly comp: Component;
|
|
166
|
+
readonly id: string;
|
|
167
|
+
readonly owner: Owner;
|
|
168
|
+
/** Last description while unsettled; null once settled. */
|
|
169
|
+
node: NativeNode | null;
|
|
170
|
+
/** Settled: identity of the last description (node, or rows array). */
|
|
171
|
+
weak: WeakRef<object> | null;
|
|
172
|
+
kind: TspKind;
|
|
173
|
+
propKeys: readonly string[];
|
|
174
|
+
childIds: readonly string[];
|
|
175
|
+
inner: Inner[];
|
|
176
|
+
rowsLines: readonly string[] | null;
|
|
177
|
+
rowsCols: number;
|
|
178
|
+
rowsNode: NativeNode | null;
|
|
179
|
+
settled: boolean;
|
|
180
|
+
settleSent: boolean;
|
|
181
|
+
frame: number;
|
|
182
|
+
/** The component whose node currently contains this one in the document (null: a region or the root). */
|
|
183
|
+
container: CompState | null;
|
|
184
|
+
/** Moved under the surface root this frame to survive a delete or a parent/child swap. */
|
|
185
|
+
parked: boolean;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
interface Walk {
|
|
189
|
+
readonly owner: Owner;
|
|
190
|
+
/** Records the owner's boundaries and hoisted overlays (null: don't record). */
|
|
191
|
+
readonly inner: Inner[] | null;
|
|
192
|
+
readonly settled: boolean;
|
|
193
|
+
readonly hoist: boolean;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
interface PendingMove {
|
|
197
|
+
readonly id: string;
|
|
198
|
+
readonly parent: string;
|
|
199
|
+
readonly before: string | null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const EMPTY_ENTRIES: readonly Entry[] = [];
|
|
203
|
+
|
|
204
|
+
/** One surface's reconciler. */
|
|
205
|
+
export class Reconciler {
|
|
206
|
+
readonly surface: string;
|
|
207
|
+
#cx: DescribeContext | undefined;
|
|
208
|
+
#states = new Map<Component, CompState>();
|
|
209
|
+
#byId = new Map<string, CompState>();
|
|
210
|
+
#frame = 0;
|
|
211
|
+
#ops: TspOp[] = [];
|
|
212
|
+
#dels: Entry[] = [];
|
|
213
|
+
#settles: string[] = [];
|
|
214
|
+
/** Selected list items sent this frame, revealed once the frame's adds have landed. */
|
|
215
|
+
#reveals: string[] = [];
|
|
216
|
+
/** Wire ids `list.selected` keys resolved to (an unresolved key names no node to reveal). */
|
|
217
|
+
#selectedIds = new Set<string>();
|
|
218
|
+
#moves: PendingMove[] = [];
|
|
219
|
+
/** Ids of components updated this frame that wait for an enclosing `add` to move in. */
|
|
220
|
+
#pendingMoves = new Set<string>();
|
|
221
|
+
/** Components parked under the root this frame. */
|
|
222
|
+
#parked: CompState[] = [];
|
|
223
|
+
#framePortals: NodeEntry[] = [];
|
|
224
|
+
#prevPortals: NodeEntry[] = [];
|
|
225
|
+
#regions: Record<RegionId, readonly Entry[]> | null = null;
|
|
226
|
+
#rows = 0;
|
|
227
|
+
#gone = new Set<string>();
|
|
228
|
+
/** Live-only regions to re-add after the surface was closed and adopted. */
|
|
229
|
+
#readd: readonly RegionId[] = [];
|
|
230
|
+
#regionOwners: Record<RegionId, Owner> = {
|
|
231
|
+
main: { base: "main", state: null },
|
|
232
|
+
dock: { base: "dock", state: null },
|
|
233
|
+
layer: { base: "layer", state: null },
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
constructor(surface: string) {
|
|
237
|
+
this.surface = surface;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** `rows` fallback nodes in the last reconciled frame. */
|
|
241
|
+
get fallbackCount(): number {
|
|
242
|
+
return this.#rows;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Diff the regions against the previous frame; the first call adds everything. */
|
|
246
|
+
reconcile(regions: NativeRegions, cx: DescribeContext): TspOp[] {
|
|
247
|
+
this.#cx = cx;
|
|
248
|
+
this.#frame++;
|
|
249
|
+
this.#ops = [];
|
|
250
|
+
this.#dels = [];
|
|
251
|
+
this.#settles = [];
|
|
252
|
+
this.#reveals = [];
|
|
253
|
+
this.#selectedIds.clear();
|
|
254
|
+
this.#framePortals = [];
|
|
255
|
+
this.#rows = 0;
|
|
256
|
+
let prev = this.#regions;
|
|
257
|
+
if (prev === null) {
|
|
258
|
+
for (const region of REGION_IDS) this.#ops.push(["add", region, this.surface, null, { id: region, k: "col" }]);
|
|
259
|
+
prev = { main: EMPTY_ENTRIES, dock: EMPTY_ENTRIES, layer: EMPTY_ENTRIES };
|
|
260
|
+
}
|
|
261
|
+
for (const region of this.#readd) this.#ops.push(["add", region, this.surface, null, { id: region, k: "col" }]);
|
|
262
|
+
this.#readd = [];
|
|
263
|
+
const next = {} as Record<RegionId, readonly Entry[]>;
|
|
264
|
+
for (const region of REGION_IDS) {
|
|
265
|
+
const hoist = region !== "layer";
|
|
266
|
+
const walk: Walk = { owner: this.#regionOwners[region], inner: null, settled: false, hoist };
|
|
267
|
+
const entries = this.#entries(regions[region], "", walk.owner, hoist, walk);
|
|
268
|
+
this.#diffChildren(region, prev[region], entries, null, walk);
|
|
269
|
+
next[region] = entries;
|
|
270
|
+
}
|
|
271
|
+
// Hoisted overlays sit before the overlay stack in `layer`.
|
|
272
|
+
const layerFirst = next.layer.length > 0 ? next.layer[0]!.id : null;
|
|
273
|
+
const portalWalk: Walk = { owner: this.#regionOwners.layer, inner: null, settled: false, hoist: false };
|
|
274
|
+
const portals = this.#framePortals.slice();
|
|
275
|
+
this.#diffChildren("layer", this.#prevPortals, portals, layerFirst, portalWalk);
|
|
276
|
+
if (this.#framePortals.length > portals.length) {
|
|
277
|
+
// Components inside a hoisted overlay described overlays of their own;
|
|
278
|
+
// only one level of hoisting exists, so those stay unsent.
|
|
279
|
+
logger.debug("TSP: overlay nested in a hoisted overlay dropped", {
|
|
280
|
+
ids: this.#framePortals.slice(portals.length).map(portal => portal.id),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
this.#prevPortals = portals;
|
|
284
|
+
this.#regions = next;
|
|
285
|
+
for (const entry of this.#dels) this.#deleteEntry(entry);
|
|
286
|
+
// Parked to survive a parent/child swap but absent from the new description.
|
|
287
|
+
for (const state of this.#parked) {
|
|
288
|
+
if (!state.parked) continue;
|
|
289
|
+
state.parked = false;
|
|
290
|
+
this.#dropState(state);
|
|
291
|
+
this.#ops.push(["del", state.id]);
|
|
292
|
+
}
|
|
293
|
+
this.#parked = [];
|
|
294
|
+
for (const id of this.#settles) this.#ops.push(["settle", id]);
|
|
295
|
+
for (const id of this.#reveals) this.#ops.push(["reveal", id, "nearest"]);
|
|
296
|
+
const ops = this.#ops;
|
|
297
|
+
this.#ops = [];
|
|
298
|
+
return this.#gone.size === 0 ? ops : ops.filter(op => !this.#touchesGone(op));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* The surface was closed (`keep:true`) and is being adopted: the terminal
|
|
303
|
+
* dropped `dock`, `layer` and focus but kept `main`. The next frame re-adds
|
|
304
|
+
* the live regions from scratch and keeps diffing `main`.
|
|
305
|
+
*/
|
|
306
|
+
detachLive(): void {
|
|
307
|
+
const regions = this.#regions;
|
|
308
|
+
if (!regions) return;
|
|
309
|
+
// A fresh frame number so every state below counts as unvisited.
|
|
310
|
+
this.#frame++;
|
|
311
|
+
for (const region of ["dock", "layer"] as const) {
|
|
312
|
+
for (const entry of regions[region]) {
|
|
313
|
+
if (entry.comp === undefined) this.#dropSubtree(entry);
|
|
314
|
+
else {
|
|
315
|
+
const state = this.#states.get(entry.comp);
|
|
316
|
+
if (state) this.#dropState(state);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
for (const portal of this.#prevPortals) this.#dropSubtree(portal);
|
|
321
|
+
this.#prevPortals = [];
|
|
322
|
+
this.#regions = { main: regions.main, dock: EMPTY_ENTRIES, layer: EMPTY_ENTRIES };
|
|
323
|
+
this.#readd = ["dock", "layer"];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** The component and keypath that described wire node `id`. */
|
|
327
|
+
target(id: string): NativeTarget | null {
|
|
328
|
+
const dot = id.indexOf(".");
|
|
329
|
+
const state = this.#byId.get(dot === -1 ? id : id.slice(0, dot));
|
|
330
|
+
if (!state) return null;
|
|
331
|
+
const keypath =
|
|
332
|
+
dot === -1
|
|
333
|
+
? ""
|
|
334
|
+
: id
|
|
335
|
+
.slice(dot + 1)
|
|
336
|
+
.split("/")
|
|
337
|
+
.map(unescapeKey)
|
|
338
|
+
.join("/");
|
|
339
|
+
return { component: state.comp, keypath };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** The key a list item node was described with (its key, else its child index). */
|
|
343
|
+
itemKey(id: string): string {
|
|
344
|
+
const dot = id.indexOf(".");
|
|
345
|
+
if (dot === -1) return this.#byId.get(id)?.node?.key ?? id;
|
|
346
|
+
const slash = id.lastIndexOf("/");
|
|
347
|
+
return unescapeKey(id.slice(Math.max(dot, slash) + 1));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Wire id of the first `editor`/`input` `component` describes (for `focus`), or null. */
|
|
351
|
+
focusTarget(component: Component): string | null {
|
|
352
|
+
const state = this.#states.get(component);
|
|
353
|
+
return state ? this.#findEditor(state, 0) : null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Stop referencing ids the terminal dropped (`gone`). */
|
|
357
|
+
forget(ids: readonly string[]): void {
|
|
358
|
+
for (const id of ids) this.#gone.add(id);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
#touchesGone(op: TspOp): boolean {
|
|
362
|
+
const gone = this.#gone;
|
|
363
|
+
switch (op[0]) {
|
|
364
|
+
case "add":
|
|
365
|
+
case "move":
|
|
366
|
+
return gone.has(op[1]) || gone.has(op[2]) || (op[3] !== null && gone.has(op[3]));
|
|
367
|
+
case "focus":
|
|
368
|
+
return op[1] !== null && gone.has(op[1]);
|
|
369
|
+
case "suspend":
|
|
370
|
+
case "resume":
|
|
371
|
+
return false;
|
|
372
|
+
default:
|
|
373
|
+
return gone.has(op[1]);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
#findEditor(state: CompState, depth: number): string | null {
|
|
378
|
+
if (depth > 64) return null;
|
|
379
|
+
const node = state.node ?? (state.weak?.deref() as NativeNode | undefined);
|
|
380
|
+
if (!node || typeof node.k !== "string") return null;
|
|
381
|
+
return this.#findEditorIn(node, state.id, "", state.owner, depth);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
#findEditorIn(node: NativeNode, id: string, keypath: string, owner: Owner, depth: number): string | null {
|
|
385
|
+
if (node.k === "editor" || node.k === "input") return id;
|
|
386
|
+
for (const entry of this.#entries(node.c, keypath, owner, true, null)) {
|
|
387
|
+
const found =
|
|
388
|
+
entry.comp === undefined
|
|
389
|
+
? this.#findEditorIn(entry.node, entry.id, entry.keypath, owner, depth)
|
|
390
|
+
: this.#findEditorOf(entry.comp, depth + 1);
|
|
391
|
+
if (found !== null) return found;
|
|
392
|
+
}
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
#findEditorOf(component: Component, depth: number): string | null {
|
|
397
|
+
const state = this.#states.get(component);
|
|
398
|
+
return state ? this.#findEditor(state, depth) : null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ── Entries ──────────────────────────────────────────────────────────
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Child slots of a node with their wire ids. Overlays are split off into the
|
|
405
|
+
* hoisted list when `hoist` is set (and a walk collects them).
|
|
406
|
+
*/
|
|
407
|
+
#entries(
|
|
408
|
+
children: readonly NativeChild[] | undefined,
|
|
409
|
+
keypath: string,
|
|
410
|
+
owner: Owner,
|
|
411
|
+
hoist: boolean,
|
|
412
|
+
walk: Walk | null,
|
|
413
|
+
): readonly Entry[] {
|
|
414
|
+
if (!children || children.length === 0) return EMPTY_ENTRIES;
|
|
415
|
+
const out: Entry[] = [];
|
|
416
|
+
let used: Set<string> | undefined;
|
|
417
|
+
for (let i = 0; i < children.length; i++) {
|
|
418
|
+
const child = children[i]!;
|
|
419
|
+
if (isComponent(child)) {
|
|
420
|
+
out.push({ id: nativeComponentId(child), comp: child });
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
const segment = child.key === undefined ? String(i) : escapeKey(child.key);
|
|
424
|
+
let path = keypath === "" ? segment : `${keypath}/${segment}`;
|
|
425
|
+
if (children.length > 1) {
|
|
426
|
+
// A key may repeat, or equal an unkeyed sibling's index.
|
|
427
|
+
used ??= new Set();
|
|
428
|
+
if (used.has(path)) {
|
|
429
|
+
let n = 2;
|
|
430
|
+
while (used.has(`${path}~${n}`)) n++;
|
|
431
|
+
path = `${path}~${n}`;
|
|
432
|
+
}
|
|
433
|
+
used.add(path);
|
|
434
|
+
}
|
|
435
|
+
const entry: NodeEntry = {
|
|
436
|
+
id: nodeId(owner.base, path),
|
|
437
|
+
node: child,
|
|
438
|
+
owner,
|
|
439
|
+
keypath: path,
|
|
440
|
+
hoist: hoist && child.k !== "overlay" && child.k !== "picker",
|
|
441
|
+
};
|
|
442
|
+
// A picker is its own sheet: hoisted like an overlay.
|
|
443
|
+
if (hoist && (child.k === "overlay" || child.k === "picker")) {
|
|
444
|
+
if (walk) {
|
|
445
|
+
// Its own id namespace (`^` marks the hoisted segment): the node now
|
|
446
|
+
// lives under `layer`, so a same-keypath node in place must not collide.
|
|
447
|
+
const portalPath = keypath === "" ? `^${path}` : `${keypath}/^${path.slice(keypath.length + 1)}`;
|
|
448
|
+
const portal: NodeEntry = {
|
|
449
|
+
...entry,
|
|
450
|
+
id: nodeId(owner.base, portalPath),
|
|
451
|
+
keypath: portalPath,
|
|
452
|
+
hoist: false,
|
|
453
|
+
};
|
|
454
|
+
walk.inner?.push({ portal });
|
|
455
|
+
this.#framePortals.push(portal);
|
|
456
|
+
}
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
out.push(entry);
|
|
460
|
+
}
|
|
461
|
+
return out;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ── Children ─────────────────────────────────────────────────────────
|
|
465
|
+
|
|
466
|
+
#diffChildren(
|
|
467
|
+
parent: string,
|
|
468
|
+
oldEntries: readonly Entry[],
|
|
469
|
+
newEntries: readonly Entry[],
|
|
470
|
+
tailBefore: string | null,
|
|
471
|
+
walk: Walk,
|
|
472
|
+
): void {
|
|
473
|
+
const oldIndex = new Map<string, number>();
|
|
474
|
+
if (oldEntries.length > 0) {
|
|
475
|
+
const keep = new Set<string>();
|
|
476
|
+
for (const entry of newEntries) keep.add(entry.id);
|
|
477
|
+
for (let i = 0; i < oldEntries.length; i++) {
|
|
478
|
+
const entry = oldEntries[i]!;
|
|
479
|
+
oldIndex.set(entry.id, i);
|
|
480
|
+
if (!keep.has(entry.id)) this.#dels.push(entry);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const count = newEntries.length;
|
|
484
|
+
const sources = newEntries.map(entry => oldIndex.get(entry.id) ?? -1);
|
|
485
|
+
const stable = stableMask(sources);
|
|
486
|
+
// Children that kept their relative order stay; every other child is
|
|
487
|
+
// placed before the nearest following one that stays. Processing left to
|
|
488
|
+
// right keeps walk order (and so hoisted-overlay order) canonical.
|
|
489
|
+
const anchors: (string | null)[] = [];
|
|
490
|
+
let anchor = tailBefore;
|
|
491
|
+
for (let i = count - 1; i >= 0; i--) {
|
|
492
|
+
anchors[i] = anchor;
|
|
493
|
+
if (stable[i]) anchor = newEntries[i]!.id;
|
|
494
|
+
}
|
|
495
|
+
for (let i = 0; i < count; i++) {
|
|
496
|
+
const entry = newEntries[i]!;
|
|
497
|
+
const source = sources[i]!;
|
|
498
|
+
const before = anchors[i]!;
|
|
499
|
+
const next = i + 1 < count ? newEntries[i + 1]!.id : tailBefore;
|
|
500
|
+
if (source < 0) {
|
|
501
|
+
this.#place(entry, parent, before, next, walk);
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (!stable[i]) {
|
|
505
|
+
const state = entry.comp === undefined ? undefined : this.#states.get(entry.comp);
|
|
506
|
+
if (state) this.#move(state, parent, before);
|
|
507
|
+
else this.#ops.push(["move", entry.id, parent, before]);
|
|
508
|
+
}
|
|
509
|
+
if (entry.comp !== undefined) this.#visitChild(entry.comp, parent, before, next, walk);
|
|
510
|
+
else this.#diffNode(oldEntries[source] as NodeEntry, entry, parent, before, walk);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Put a child that wasn't under `parent` last frame before `before`.
|
|
516
|
+
* `next` is its following sibling, recorded for later re-adds.
|
|
517
|
+
*/
|
|
518
|
+
#place(entry: Entry, parent: string, before: string | null, next: string | null, walk: Walk): void {
|
|
519
|
+
if (entry.comp === undefined) {
|
|
520
|
+
this.#ops.push(["add", entry.id, parent, before, this.#materialize(entry, this.#walkFor(entry, walk))]);
|
|
521
|
+
this.#flushMoves();
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const state = this.#states.get(entry.comp);
|
|
525
|
+
if (state?.frame === this.#frame) {
|
|
526
|
+
this.#warnDuplicate(state);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
walk.inner?.push({ comp: entry.comp, parent, before: next });
|
|
530
|
+
if (state) {
|
|
531
|
+
this.#move(state, parent, before);
|
|
532
|
+
this.#visit(state, parent, before, walk.settled);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
this.#addComponent(entry.comp, parent, before, walk.settled);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Visit a component whose document position is already right. `before` is
|
|
540
|
+
* an anchor present in the document now (for a root re-add); `next` is its
|
|
541
|
+
* following sibling, recorded for later frames.
|
|
542
|
+
*/
|
|
543
|
+
#visitChild(comp: Component, parent: string, before: string | null, next: string | null, walk: Walk): void {
|
|
544
|
+
const state = this.#states.get(comp);
|
|
545
|
+
if (state?.frame === this.#frame) {
|
|
546
|
+
this.#warnDuplicate(state);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
walk.inner?.push({ comp, parent, before: next });
|
|
550
|
+
if (state) this.#visit(state, parent, before, walk.settled);
|
|
551
|
+
else this.#addComponent(comp, parent, before, walk.settled);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
#addComponent(comp: Component, parent: string, before: string | null, settled: boolean): void {
|
|
555
|
+
const state = this.#createState(comp, parent);
|
|
556
|
+
this.#ops.push(["add", state.id, parent, before, this.#materializeComponent(state, settled)]);
|
|
557
|
+
this.#flushMoves();
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
#warnDuplicate(state: CompState): void {
|
|
561
|
+
logger.warn("TSP: component described twice in one frame; later occurrence skipped", {
|
|
562
|
+
id: state.id,
|
|
563
|
+
kind: state.comp.constructor.name,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
#walkFor(entry: NodeEntry, walk: Walk): Walk {
|
|
568
|
+
return entry.hoist === walk.hoist ? walk : { ...walk, hoist: entry.hoist };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// ── Nodes ────────────────────────────────────────────────────────────
|
|
572
|
+
|
|
573
|
+
#diffNode(old: NodeEntry, next: NodeEntry, parent: string, before: string | null, walk: Walk): void {
|
|
574
|
+
const inner = this.#walkFor(next, walk);
|
|
575
|
+
if (old.node === next.node) {
|
|
576
|
+
this.#revisitNode(next, inner);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (old.node.k !== next.node.k) {
|
|
580
|
+
this.#dropSubtree(old);
|
|
581
|
+
this.#ops.push(["del", next.id]);
|
|
582
|
+
this.#ops.push(["add", next.id, parent, before, this.#materialize(next, inner)]);
|
|
583
|
+
this.#flushMoves();
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
const oldEntries = this.#entries(old.node.c, old.keypath, old.owner, old.hoist, null);
|
|
587
|
+
const newEntries = this.#entries(next.node.c, next.keypath, next.owner, next.hoist, inner);
|
|
588
|
+
this.#diffProps(
|
|
589
|
+
next.id,
|
|
590
|
+
next.node.k,
|
|
591
|
+
this.#wireProps(old.node, old.owner, oldEntries),
|
|
592
|
+
this.#wireProps(next.node, next.owner, newEntries),
|
|
593
|
+
);
|
|
594
|
+
this.#diffChildren(next.id, oldEntries, newEntries, null, inner);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** Walk an unchanged node for the component boundaries and overlays inside it. */
|
|
598
|
+
#revisitNode(entry: NodeEntry, walk: Walk): void {
|
|
599
|
+
const entries = this.#entries(entry.node.c, entry.keypath, entry.owner, entry.hoist, walk);
|
|
600
|
+
for (let i = 0; i < entries.length; i++) {
|
|
601
|
+
const child = entries[i]!;
|
|
602
|
+
const before = i + 1 < entries.length ? entries[i + 1]!.id : null;
|
|
603
|
+
if (child.comp === undefined) this.#revisitNode(child, this.#walkFor(child, walk));
|
|
604
|
+
else this.#visitChild(child.comp, entry.id, before, before, walk);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
#diffProps(
|
|
609
|
+
id: string,
|
|
610
|
+
kind: TspKind,
|
|
611
|
+
oldProps: Readonly<Record<string, unknown>> | undefined,
|
|
612
|
+
newProps: Readonly<Record<string, unknown>> | undefined,
|
|
613
|
+
): void {
|
|
614
|
+
const textKind = TEXT_KINDS.has(kind);
|
|
615
|
+
let set: Record<string, unknown> | undefined;
|
|
616
|
+
if (newProps) {
|
|
617
|
+
for (const key in newProps) {
|
|
618
|
+
const value = newProps[key];
|
|
619
|
+
if (value === undefined || (textKind && key === "text")) continue;
|
|
620
|
+
if (!sameValue(oldProps?.[key], value)) (set ??= {})[key] = value;
|
|
621
|
+
}
|
|
622
|
+
if (kind === "list") this.#noteSelected(set?.selected);
|
|
623
|
+
}
|
|
624
|
+
if (oldProps) {
|
|
625
|
+
for (const key in oldProps) {
|
|
626
|
+
if (oldProps[key] === undefined || newProps?.[key] !== undefined) continue;
|
|
627
|
+
if (textKind && key === "text") continue;
|
|
628
|
+
(set ??= {})[key] = null;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (textKind) {
|
|
632
|
+
const oldText = oldProps?.text as string | undefined;
|
|
633
|
+
const newText = newProps?.text as string | undefined;
|
|
634
|
+
if (newText === undefined) {
|
|
635
|
+
if (oldText !== undefined) (set ??= {}).text = null;
|
|
636
|
+
} else if (newText !== oldText) {
|
|
637
|
+
this.#textOp(id, kind, oldText ?? "", newText);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (set) this.#ops.push(["set", id, set]);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
#textOp(id: string, kind: TspKind, oldText: string, newText: string): void {
|
|
644
|
+
if (newText.length >= oldText.length && newText.startsWith(oldText)) {
|
|
645
|
+
this.#ops.push(["text", id, "append", newText.slice(oldText.length)]);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
if ((kind === "editor" || kind === "input") && oldText.length > 0) {
|
|
649
|
+
const limit = Math.min(oldText.length, newText.length);
|
|
650
|
+
let prefix = 0;
|
|
651
|
+
while (prefix < limit && oldText.charCodeAt(prefix) === newText.charCodeAt(prefix)) prefix++;
|
|
652
|
+
let suffix = 0;
|
|
653
|
+
while (
|
|
654
|
+
suffix < limit - prefix &&
|
|
655
|
+
oldText.charCodeAt(oldText.length - 1 - suffix) === newText.charCodeAt(newText.length - 1 - suffix)
|
|
656
|
+
)
|
|
657
|
+
suffix++;
|
|
658
|
+
this.#ops.push([
|
|
659
|
+
"splice",
|
|
660
|
+
id,
|
|
661
|
+
prefix,
|
|
662
|
+
oldText.length - prefix - suffix,
|
|
663
|
+
newText.slice(prefix, newText.length - suffix),
|
|
664
|
+
]);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
this.#ops.push(["text", id, "replace", newText]);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Props as sent: item keys, header children and anchor keypaths resolved to
|
|
672
|
+
* wire ids, and icon glyphs split into `icon` spans.
|
|
673
|
+
*/
|
|
674
|
+
#wireProps(
|
|
675
|
+
node: NativeNode,
|
|
676
|
+
owner: Owner,
|
|
677
|
+
entries: readonly Entry[],
|
|
678
|
+
): Readonly<Record<string, unknown>> | undefined {
|
|
679
|
+
return normalizeIconProps(node.k, this.#resolveRefs(node, owner, entries));
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
#resolveRefs(
|
|
683
|
+
node: NativeNode,
|
|
684
|
+
owner: Owner,
|
|
685
|
+
entries: readonly Entry[],
|
|
686
|
+
): Readonly<Record<string, unknown>> | undefined {
|
|
687
|
+
const props = asProps(node);
|
|
688
|
+
switch (node.k) {
|
|
689
|
+
case "list": {
|
|
690
|
+
const selected = props?.selected;
|
|
691
|
+
if (typeof selected !== "string") return props;
|
|
692
|
+
const item = entries.find(entry => entry.comp === undefined && entry.node.key === selected);
|
|
693
|
+
if (!item) return props;
|
|
694
|
+
this.#selectedIds.add(item.id);
|
|
695
|
+
return { ...props, selected: item.id };
|
|
696
|
+
}
|
|
697
|
+
case "card":
|
|
698
|
+
case "section": {
|
|
699
|
+
if (props?.head !== undefined) return props;
|
|
700
|
+
const head = entries.find(entry => entry.comp === undefined && entry.node.key === "head");
|
|
701
|
+
return head ? { ...props, head: head.id } : props;
|
|
702
|
+
}
|
|
703
|
+
case "overlay": {
|
|
704
|
+
const anchor = props?.anchor;
|
|
705
|
+
if (owner.state === null || typeof anchor !== "object" || anchor === null) return props;
|
|
706
|
+
if ("caret" in anchor && typeof anchor.caret === "string")
|
|
707
|
+
return { ...props, anchor: { caret: nodeId(owner.base, anchor.caret) } };
|
|
708
|
+
if ("node" in anchor && typeof anchor.node === "string")
|
|
709
|
+
return { ...props, anchor: { ...anchor, node: nodeId(owner.base, anchor.node) } };
|
|
710
|
+
return props;
|
|
711
|
+
}
|
|
712
|
+
default:
|
|
713
|
+
return props;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** A full wire subtree for an entry; nested components get states (or pending moves when mounted elsewhere). */
|
|
718
|
+
#materialize(entry: NodeEntry, walk: Walk): TspNode {
|
|
719
|
+
const entries = this.#entries(entry.node.c, entry.keypath, entry.owner, entry.hoist, walk);
|
|
720
|
+
const children: TspNode[] = [];
|
|
721
|
+
for (let i = 0; i < entries.length; i++) {
|
|
722
|
+
const child = entries[i]!;
|
|
723
|
+
if (child.comp === undefined) {
|
|
724
|
+
children.push(this.#materialize(child, this.#walkFor(child, walk)));
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
let state = this.#states.get(child.comp);
|
|
728
|
+
if (state?.frame === this.#frame) {
|
|
729
|
+
this.#warnDuplicate(state);
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
const before = i + 1 < entries.length ? entries[i + 1]!.id : null;
|
|
733
|
+
walk.inner?.push({ comp: child.comp, parent: entry.id, before });
|
|
734
|
+
if (state) {
|
|
735
|
+
const next = this.#resolve(state);
|
|
736
|
+
if (next.k === (state.node?.k ?? state.kind)) {
|
|
737
|
+
// Mounted elsewhere: update it in place now, then move it in once
|
|
738
|
+
// this subtree lands, keeping its terminal-side state. Its own adds
|
|
739
|
+
// flush their moves; ours wait for the enclosing add.
|
|
740
|
+
// Pending before the visit: a delete during it that would take this
|
|
741
|
+
// component along parks it instead.
|
|
742
|
+
this.#pendingMoves.add(state.id);
|
|
743
|
+
const pending = this.#moves;
|
|
744
|
+
this.#moves = [];
|
|
745
|
+
this.#visit(state, entry.id, before, walk.settled, next);
|
|
746
|
+
this.#moves = pending;
|
|
747
|
+
pending.push({ id: state.id, parent: entry.id, before });
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
// Its root kind changed too: nothing worth keeping, rebuild it here.
|
|
751
|
+
this.#dropState(state);
|
|
752
|
+
this.#ops.push(["del", state.id]);
|
|
753
|
+
}
|
|
754
|
+
state = this.#createState(child.comp, entry.id);
|
|
755
|
+
children.push(this.#materializeComponent(state, walk.settled));
|
|
756
|
+
}
|
|
757
|
+
return this.#wire(entry.id, entry.node.k, this.#wireProps(entry.node, entry.owner, entries), children);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
#wire(
|
|
761
|
+
id: string,
|
|
762
|
+
k: TspKind,
|
|
763
|
+
props: Readonly<Record<string, unknown>> | undefined,
|
|
764
|
+
children: readonly TspNode[],
|
|
765
|
+
): TspNode {
|
|
766
|
+
if (k === "list") this.#noteSelected(props?.selected);
|
|
767
|
+
const out: { id: string; k: TspKind; p?: Record<string, unknown>; c?: readonly TspNode[] } = { id, k };
|
|
768
|
+
if (props) {
|
|
769
|
+
let p: Record<string, unknown> | undefined;
|
|
770
|
+
for (const key in props) {
|
|
771
|
+
const value = props[key];
|
|
772
|
+
if (value !== undefined && value !== null) (p ??= {})[key] = value;
|
|
773
|
+
}
|
|
774
|
+
if (p) out.p = p;
|
|
775
|
+
}
|
|
776
|
+
if (children.length > 0) out.c = children;
|
|
777
|
+
return out as TspNode;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/** A list's selection went out (on add or change): scroll it into view at frame end. */
|
|
781
|
+
#noteSelected(selected: unknown): void {
|
|
782
|
+
if (typeof selected === "string" && this.#selectedIds.has(selected)) this.#reveals.push(selected);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/** Move components an `add` left out into their new parent, last first so every `before` exists. */
|
|
786
|
+
#flushMoves(): void {
|
|
787
|
+
const moves = this.#moves;
|
|
788
|
+
if (moves.length === 0) return;
|
|
789
|
+
this.#moves = [];
|
|
790
|
+
for (let i = moves.length - 1; i >= 0; i--) {
|
|
791
|
+
const move = moves[i]!;
|
|
792
|
+
const state = this.#byId.get(move.id);
|
|
793
|
+
if (state) this.#move(state, move.parent, move.before);
|
|
794
|
+
else this.#ops.push(["move", move.id, move.parent, move.before]);
|
|
795
|
+
this.#pendingMoves.delete(move.id);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/** The component whose description holds node `parent` in the document (null: region, root, or `layer` via hoisting). */
|
|
800
|
+
#containerOf(parent: string): CompState | null {
|
|
801
|
+
if (parent.includes("^")) return null;
|
|
802
|
+
const dot = parent.indexOf(".");
|
|
803
|
+
return this.#byId.get(dot === -1 ? parent : parent.slice(0, dot)) ?? null;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Move a component. When the target currently sits inside the component
|
|
808
|
+
* itself (a parent and child swapped places this frame), first park the
|
|
809
|
+
* component on that path under the root; it moves to its own new place
|
|
810
|
+
* later in the frame.
|
|
811
|
+
*/
|
|
812
|
+
#move(state: CompState, parent: string, before: string | null): void {
|
|
813
|
+
const target = this.#containerOf(parent);
|
|
814
|
+
// The first component on the path that still has to move this frame
|
|
815
|
+
// (not yet visited, or waiting for an enclosing add): parking it breaks
|
|
816
|
+
// the cycle without disturbing anything already in its final place.
|
|
817
|
+
let movable: CompState | null = null;
|
|
818
|
+
for (let at = target, guard = 0; at && guard < 4096; at = at.container, guard++) {
|
|
819
|
+
if (at === state) {
|
|
820
|
+
if (movable) {
|
|
821
|
+
this.#ops.push(["move", movable.id, this.surface, null]);
|
|
822
|
+
movable.container = null;
|
|
823
|
+
movable.parked = true;
|
|
824
|
+
this.#parked.push(movable);
|
|
825
|
+
}
|
|
826
|
+
break;
|
|
827
|
+
}
|
|
828
|
+
if (!movable && (at.frame !== this.#frame || this.#pendingMoves.has(at.id))) movable = at;
|
|
829
|
+
}
|
|
830
|
+
this.#ops.push(["move", state.id, parent, before]);
|
|
831
|
+
state.container = target;
|
|
832
|
+
state.parked = false;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// ── Components ───────────────────────────────────────────────────────
|
|
836
|
+
|
|
837
|
+
#createState(comp: Component, parent: string): CompState {
|
|
838
|
+
const id = nativeComponentId(comp);
|
|
839
|
+
const owner: { base: string; state: CompState | null } = { base: id, state: null };
|
|
840
|
+
const state: CompState = {
|
|
841
|
+
comp,
|
|
842
|
+
id,
|
|
843
|
+
owner,
|
|
844
|
+
node: null,
|
|
845
|
+
weak: null,
|
|
846
|
+
kind: "col",
|
|
847
|
+
propKeys: [],
|
|
848
|
+
childIds: [],
|
|
849
|
+
inner: [],
|
|
850
|
+
rowsLines: null,
|
|
851
|
+
rowsCols: 0,
|
|
852
|
+
rowsNode: null,
|
|
853
|
+
settled: false,
|
|
854
|
+
settleSent: false,
|
|
855
|
+
frame: 0,
|
|
856
|
+
container: this.#containerOf(parent),
|
|
857
|
+
parked: false,
|
|
858
|
+
};
|
|
859
|
+
owner.state = state;
|
|
860
|
+
this.#states.set(comp, state);
|
|
861
|
+
this.#byId.set(id, state);
|
|
862
|
+
return state;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/** The component's description, or a `rows` node rendered at `cx.cols`. */
|
|
866
|
+
#resolve(state: CompState): NativeNode {
|
|
867
|
+
const comp = state.comp;
|
|
868
|
+
const describe = comp.describe;
|
|
869
|
+
// A Container subclass that paints its own rows but inherits the plain
|
|
870
|
+
// column description would lose its rendering: fall back to rows.
|
|
871
|
+
// (Read lazily: this module and tui.ts import each other.)
|
|
872
|
+
const inherited = describe === Container.prototype.describe && comp.render !== Container.prototype.render;
|
|
873
|
+
if (describe && !inherited) {
|
|
874
|
+
const node = describe.call(comp, this.#cx!);
|
|
875
|
+
if (node) return node;
|
|
876
|
+
}
|
|
877
|
+
this.#rows++;
|
|
878
|
+
const cols = Math.max(1, this.#cx!.cols);
|
|
879
|
+
const lines = comp.render(cols);
|
|
880
|
+
if (state.rowsNode && lines === state.rowsLines && cols === state.rowsCols) return state.rowsNode;
|
|
881
|
+
state.rowsLines = lines;
|
|
882
|
+
state.rowsCols = cols;
|
|
883
|
+
const clean = lines.some(line => line.includes(CURSOR_MARKER))
|
|
884
|
+
? lines.map(line => line.replaceAll(CURSOR_MARKER, ""))
|
|
885
|
+
: lines;
|
|
886
|
+
state.rowsNode = { k: "rows", p: { cols, lines: clean } };
|
|
887
|
+
return state.rowsNode;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
#identity(state: CompState, node: NativeNode): object {
|
|
891
|
+
return node === state.rowsNode && state.rowsLines ? state.rowsLines : node;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
#rootEntry(state: CompState, node: NativeNode): NodeEntry {
|
|
895
|
+
return { id: state.id, node, owner: state.owner, keypath: "", hoist: true };
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
#materializeComponent(state: CompState, settledAbove: boolean): TspNode {
|
|
899
|
+
state.frame = this.#frame;
|
|
900
|
+
const node = this.#resolve(state);
|
|
901
|
+
const settled = settledAbove || state.settled || isNativeSettled(state.comp);
|
|
902
|
+
const inner: Inner[] = [];
|
|
903
|
+
const wire = this.#materialize(this.#rootEntry(state, node), { owner: state.owner, inner, settled, hoist: true });
|
|
904
|
+
state.inner = inner;
|
|
905
|
+
state.node = node;
|
|
906
|
+
state.weak = null;
|
|
907
|
+
state.settleSent = false;
|
|
908
|
+
this.#finish(state, node, settled);
|
|
909
|
+
return wire;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
#visit(
|
|
913
|
+
state: CompState,
|
|
914
|
+
parent: string,
|
|
915
|
+
before: string | null,
|
|
916
|
+
settledAbove: boolean,
|
|
917
|
+
next: NativeNode = this.#resolve(state),
|
|
918
|
+
): void {
|
|
919
|
+
state.frame = this.#frame;
|
|
920
|
+
const settled = settledAbove || state.settled || isNativeSettled(state.comp);
|
|
921
|
+
if (state.node !== null) {
|
|
922
|
+
if (next === state.node) this.#revisitComponent(state, settled);
|
|
923
|
+
else {
|
|
924
|
+
const inner: Inner[] = [];
|
|
925
|
+
const walk: Walk = { owner: state.owner, inner, settled, hoist: true };
|
|
926
|
+
this.#diffNode(this.#rootEntry(state, state.node), this.#rootEntry(state, next), parent, before, walk);
|
|
927
|
+
state.inner = inner;
|
|
928
|
+
state.node = next;
|
|
929
|
+
}
|
|
930
|
+
} else if (state.weak?.deref() === this.#identity(state, next)) {
|
|
931
|
+
this.#revisitComponent(state, settled);
|
|
932
|
+
} else {
|
|
933
|
+
this.#replaceSettled(state, next, parent, before, settled);
|
|
934
|
+
}
|
|
935
|
+
this.#finish(state, next, settled);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/** Replay an unchanged component's walk: visit its boundaries, re-offer its overlays. */
|
|
939
|
+
#revisitComponent(state: CompState, settled: boolean): void {
|
|
940
|
+
const walk: Walk = { owner: state.owner, inner: null, settled, hoist: true };
|
|
941
|
+
for (const item of state.inner) {
|
|
942
|
+
if (item.comp === undefined) this.#framePortals.push(item.portal);
|
|
943
|
+
else this.#visitChild(item.comp, item.parent, item.before, item.before, walk);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/** A settled component changed: targeted ops by id, no diff against dropped state. */
|
|
948
|
+
#replaceSettled(state: CompState, next: NativeNode, parent: string, before: string | null, settled: boolean): void {
|
|
949
|
+
state.settleSent = false;
|
|
950
|
+
const inner: Inner[] = [];
|
|
951
|
+
const walk: Walk = { owner: state.owner, inner, settled, hoist: true };
|
|
952
|
+
if (next.k !== state.kind) {
|
|
953
|
+
this.#dropNested(state);
|
|
954
|
+
this.#ops.push(["del", state.id]);
|
|
955
|
+
this.#ops.push(["add", state.id, parent, before, this.#materialize(this.#rootEntry(state, next), walk)]);
|
|
956
|
+
this.#flushMoves();
|
|
957
|
+
state.inner = inner;
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
const entries = this.#entries(next.c, "", state.owner, true, walk);
|
|
961
|
+
const props = this.#wireProps(next, state.owner, entries);
|
|
962
|
+
const textKind = TEXT_KINDS.has(next.k);
|
|
963
|
+
let set: Record<string, unknown> | undefined;
|
|
964
|
+
if (props) {
|
|
965
|
+
for (const key in props) {
|
|
966
|
+
const value = props[key];
|
|
967
|
+
if (value === undefined || (textKind && key === "text")) continue;
|
|
968
|
+
(set ??= {})[key] = value;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
for (const key of state.propKeys) {
|
|
972
|
+
if (props?.[key] === undefined) (set ??= {})[key] = null;
|
|
973
|
+
}
|
|
974
|
+
if (set) this.#ops.push(["set", state.id, set]);
|
|
975
|
+
if (next.k === "list") this.#noteSelected(set?.selected);
|
|
976
|
+
if (textKind && typeof props?.text === "string") this.#ops.push(["text", state.id, "replace", props.text]);
|
|
977
|
+
this.#dropNested(state);
|
|
978
|
+
for (const childId of state.childIds) {
|
|
979
|
+
// A child component updated elsewhere this frame, or parked, no longer sits here.
|
|
980
|
+
const child = this.#byId.get(childId);
|
|
981
|
+
if (child?.frame !== this.#frame && !child?.parked) this.#ops.push(["del", childId]);
|
|
982
|
+
}
|
|
983
|
+
this.#diffChildren(state.id, EMPTY_ENTRIES, entries, null, walk);
|
|
984
|
+
state.inner = inner;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/** Send the settle hint once, then keep only what targeted updates need. */
|
|
988
|
+
#finish(state: CompState, node: NativeNode, settled: boolean): void {
|
|
989
|
+
if (!settled) return;
|
|
990
|
+
state.settled = true;
|
|
991
|
+
if (!state.settleSent) {
|
|
992
|
+
state.settleSent = true;
|
|
993
|
+
this.#settles.push(state.id);
|
|
994
|
+
}
|
|
995
|
+
const entries = this.#entries(node.c, "", state.owner, true, null);
|
|
996
|
+
const props = this.#wireProps(node, state.owner, entries);
|
|
997
|
+
const keys: string[] = [];
|
|
998
|
+
if (props) for (const key in props) if (props[key] !== undefined) keys.push(key);
|
|
999
|
+
state.kind = node.k;
|
|
1000
|
+
state.propKeys = keys;
|
|
1001
|
+
state.childIds = entries.map(entry => entry.id);
|
|
1002
|
+
state.weak = new WeakRef(this.#identity(state, node));
|
|
1003
|
+
state.node = null;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// ── Removal ──────────────────────────────────────────────────────────
|
|
1007
|
+
|
|
1008
|
+
#deleteEntry(entry: Entry): void {
|
|
1009
|
+
if (entry.comp !== undefined) {
|
|
1010
|
+
const state = this.#states.get(entry.comp);
|
|
1011
|
+
if (state?.frame === this.#frame) return;
|
|
1012
|
+
if (state) this.#dropState(state);
|
|
1013
|
+
this.#ops.push(["del", entry.id]);
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
this.#dropSubtree(entry);
|
|
1017
|
+
this.#ops.push(["del", entry.id]);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Forget the components inside a subtree about to be deleted. Call before
|
|
1022
|
+
* emitting its `del`: a component already updated this frame and waiting to
|
|
1023
|
+
* move into a new subtree is parked under the surface root first, so the
|
|
1024
|
+
* delete doesn't take it along.
|
|
1025
|
+
*/
|
|
1026
|
+
#dropSubtree(entry: NodeEntry): void {
|
|
1027
|
+
for (const child of this.#entries(entry.node.c, entry.keypath, entry.owner, entry.hoist, null)) {
|
|
1028
|
+
if (child.comp === undefined) this.#dropSubtree(child);
|
|
1029
|
+
else {
|
|
1030
|
+
const state = this.#states.get(child.comp);
|
|
1031
|
+
if (state) this.#dropOrPark(state);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
#dropNested(state: CompState): void {
|
|
1037
|
+
for (const item of state.inner) {
|
|
1038
|
+
if (item.comp === undefined) continue;
|
|
1039
|
+
const child = this.#states.get(item.comp);
|
|
1040
|
+
if (child) this.#dropOrPark(child);
|
|
1041
|
+
}
|
|
1042
|
+
state.inner = [];
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
#dropOrPark(state: CompState): void {
|
|
1046
|
+
// Already parked: no longer inside the subtree being deleted.
|
|
1047
|
+
if (state.parked) return;
|
|
1048
|
+
if (state.frame !== this.#frame) this.#dropState(state);
|
|
1049
|
+
else if (this.#pendingMoves.has(state.id)) {
|
|
1050
|
+
this.#ops.push(["move", state.id, this.surface, null]);
|
|
1051
|
+
state.container = null;
|
|
1052
|
+
state.parked = true;
|
|
1053
|
+
this.#parked.push(state);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
#dropState(state: CompState): void {
|
|
1058
|
+
state.parked = false;
|
|
1059
|
+
this.#states.delete(state.comp);
|
|
1060
|
+
this.#byId.delete(state.id);
|
|
1061
|
+
this.#dropNested(state);
|
|
1062
|
+
}
|
|
1063
|
+
}
|