@linxiraos/pi-tui 1.1.27 → 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 +47 -463
- package/README.md +7 -5
- package/dist/types/app-keybindings.d.ts +1 -1
- package/dist/types/apps/cleanse-board.d.ts +1 -1
- package/dist/types/apps/debug/log-viewer.d.ts +4 -0
- package/dist/types/apps/debug/raw-sse-buffer.d.ts +2 -0
- package/dist/types/apps/git/git-tui.d.ts +2 -2
- package/dist/types/apps/git/help.d.ts +3 -1
- package/dist/types/apps/if-bench-board.d.ts +2 -2
- package/dist/types/apps/setup-model-picker.d.ts +1 -1
- package/dist/types/autocomplete.d.ts +5 -2
- package/dist/types/charts/chart-plan.d.ts +674 -0
- package/dist/types/charts/chart-svg.d.ts +59 -0
- package/dist/types/charts/table-data.d.ts +175 -0
- package/dist/types/chat/assistant-message.d.ts +5 -0
- package/dist/types/chat/chat-transcript-builder.d.ts +2 -0
- package/dist/types/chat/compaction-summary-message.d.ts +1 -0
- package/dist/types/chat/execution-shared.d.ts +6 -5
- package/dist/types/chat/fence-figure.d.ts +42 -0
- package/dist/types/chat/figure-markdown.d.ts +42 -0
- package/dist/types/chat/messages.d.ts +1 -1
- package/dist/types/chat/svg-figure.d.ts +33 -0
- package/dist/types/chat/svg-source.d.ts +49 -0
- package/dist/types/chat/table-chart.d.ts +89 -0
- package/dist/types/chat/tool-execution.d.ts +2 -0
- package/dist/types/chat/user-message.d.ts +15 -2
- package/dist/types/chrome/shared.d.ts +9 -0
- package/dist/types/chrome/transcript-container.d.ts +6 -0
- package/dist/types/components/box.d.ts +1 -0
- package/dist/types/components/disclosure.d.ts +1 -0
- package/dist/types/components/editor.d.ts +15 -4
- package/dist/types/components/form.d.ts +1 -0
- package/dist/types/components/image.d.ts +12 -44
- package/dist/types/components/key-value-list.d.ts +1 -0
- package/dist/types/components/layout/row.d.ts +1 -0
- package/dist/types/components/layout/stack.d.ts +1 -0
- package/dist/types/components/markdown.d.ts +36 -3
- package/dist/types/components/scroll-view.d.ts +1 -0
- package/dist/types/components/section.d.ts +1 -0
- package/dist/types/components/table.d.ts +1 -0
- package/dist/types/components/text.d.ts +1 -0
- package/dist/types/components/tree-view.d.ts +7 -0
- package/dist/types/components/truncated-text.d.ts +1 -0
- package/dist/types/fuzzy.d.ts +29 -1
- package/dist/types/key-hint-format.d.ts +1 -1
- package/dist/types/keybinding-matchers.d.ts +3 -0
- package/dist/types/keybindings.d.ts +1 -1
- package/dist/types/kitty-graphics.d.ts +2 -2
- package/dist/types/loop-watchdog.d.ts +8 -7
- package/dist/types/multiplexers/cmux.d.ts +16 -0
- package/dist/types/multiplexers/herdr.d.ts +26 -0
- package/dist/types/multiplexers/notify.d.ts +13 -0
- package/dist/types/multiplexers/orca.d.ts +12 -0
- package/dist/types/multiplexers/screen.d.ts +9 -0
- package/dist/types/multiplexers/tmux.d.ts +29 -0
- package/dist/types/multiplexers/types.d.ts +100 -0
- package/dist/types/multiplexers/wmux.d.ts +12 -0
- package/dist/types/multiplexers/zellij.d.ts +12 -0
- package/dist/types/native/backend.d.ts +3 -2
- package/dist/types/native/blobs.d.ts +7 -3
- package/dist/types/native/encode.d.ts +3 -4
- package/dist/types/native/overlay.d.ts +5 -0
- package/dist/types/overlays/agent-hub-projection.d.ts +6 -5
- package/dist/types/overlays/agent-hub-types.d.ts +9 -0
- package/dist/types/overlays/agent-hub.d.ts +2 -0
- package/dist/types/overlays/agent-transcript-viewer.d.ts +11 -2
- package/dist/types/overlays/agents-hub.d.ts +9 -0
- package/dist/types/overlays/annotation-overlay.d.ts +19 -2
- package/dist/types/overlays/annotation-types.d.ts +2 -0
- package/dist/types/overlays/ask-dialog.d.ts +5 -2
- package/dist/types/overlays/history-search.d.ts +10 -0
- package/dist/types/overlays/hook-selector.d.ts +9 -4
- package/dist/types/overlays/jobs-panel.d.ts +1 -7
- package/dist/types/overlays/model-browser.d.ts +54 -7
- package/dist/types/overlays/model-hub.d.ts +23 -0
- package/dist/types/overlays/oauth-selector.d.ts +0 -1
- package/dist/types/overlays/plan-review-overlay.d.ts +3 -2
- package/dist/types/overlays/session-observer-registry.d.ts +2 -0
- package/dist/types/overlays/session-selector.d.ts +6 -2
- package/dist/types/overlays/settings-defs.d.ts +1 -0
- package/dist/types/overlays/usage-dashboard.d.ts +8 -4
- package/dist/types/overlays/usage-display.d.ts +3 -1
- package/dist/types/prompt/composer.d.ts +1 -1
- package/dist/types/prompt/custom-editor.d.ts +11 -8
- package/dist/types/prompt/image-source.d.ts +2 -1
- package/dist/types/prompt/interactive-context-helpers.d.ts +4 -1
- package/dist/types/prompt/model-mention-autocomplete.d.ts +1 -1
- package/dist/types/prompt/welcome.d.ts +6 -1
- 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/status-line/component.d.ts +3 -0
- package/dist/types/status-line/host.d.ts +4 -1
- package/dist/types/status-line/segments.d.ts +2 -0
- package/dist/types/status-line/types.d.ts +14 -3
- package/dist/types/terminal-capabilities.d.ts +27 -35
- package/dist/types/terminal-multiplexer.d.ts +127 -14
- package/dist/types/terminal.d.ts +22 -6
- package/dist/types/theme/active-symbols.d.ts +1 -1
- package/dist/types/theme/schema.d.ts +11 -2
- package/dist/types/theme/symbols.d.ts +1 -1
- package/dist/types/theme/theme-class.d.ts +7 -0
- package/dist/types/theme/theme.d.ts +1 -1
- package/dist/types/theme/tui-adapters.d.ts +3 -0
- package/dist/types/tmux.d.ts +11 -4
- package/dist/types/tools/daemon.d.ts +1 -1
- package/dist/types/tools/eval.d.ts +22 -3
- package/dist/types/tools/glob.d.ts +4 -0
- package/dist/types/tools/native-view.d.ts +47 -0
- package/dist/types/tools/output-meta.d.ts +11 -1
- package/dist/types/tools/renderer.d.ts +37 -2
- package/dist/types/tools/streaming-output.d.ts +20 -1
- package/dist/types/tools/task.d.ts +0 -4
- package/dist/types/tools/write.d.ts +9 -2
- package/dist/types/tools/yield.d.ts +22 -0
- package/dist/types/tui.d.ts +31 -0
- package/dist/types/utils.d.ts +6 -0
- package/dist/types/vim.d.ts +19 -5
- package/package.json +13 -9
- package/src/app-keybindings.d.ts +1 -1
- package/src/app-keybindings.ts +1 -1
- package/src/apps/autoresearch-dashboard.ts +7 -3
- package/src/apps/cleanse-board.ts +2 -2
- package/src/apps/cleanse-picker.ts +1 -1
- package/src/apps/debug/log-viewer.ts +188 -72
- package/src/apps/debug/raw-sse-buffer.ts +8 -0
- package/src/apps/debug/raw-sse.ts +39 -5
- package/src/apps/git/diff-pane.ts +26 -5
- package/src/apps/git/git-tui.ts +2 -2
- package/src/apps/git/help.ts +29 -20
- package/src/apps/if-bench-board.ts +2 -2
- package/src/apps/ps-top.ts +2 -2
- package/src/apps/setup-model-picker.ts +1 -1
- package/src/autocomplete.d.ts +5 -2
- package/src/autocomplete.ts +67 -43
- package/src/charts/chart-plan.d.ts +674 -0
- package/src/charts/chart-plan.ts +5081 -0
- package/src/charts/chart-svg.d.ts +59 -0
- package/src/charts/chart-svg.ts +4657 -0
- package/src/charts/table-data.d.ts +175 -0
- package/src/charts/table-data.ts +1478 -0
- package/src/chat/advisor-message.ts +11 -0
- package/src/chat/assistant-message.d.ts +5 -0
- package/src/chat/assistant-message.ts +219 -50
- package/src/chat/chat-transcript-builder.d.ts +2 -0
- package/src/chat/chat-transcript-builder.ts +5 -3
- package/src/chat/compaction-summary-message.d.ts +1 -0
- package/src/chat/compaction-summary-message.ts +8 -0
- package/src/chat/execution-shared.d.ts +6 -5
- package/src/chat/execution-shared.ts +42 -25
- package/src/chat/fence-figure.d.ts +42 -0
- package/src/chat/fence-figure.ts +80 -0
- package/src/chat/figure-markdown.d.ts +42 -0
- package/src/chat/figure-markdown.ts +174 -0
- package/src/chat/messages.d.ts +1 -1
- package/src/chat/messages.ts +1 -1
- package/src/chat/read-tool-group.ts +6 -4
- package/src/chat/skill-message.ts +6 -0
- package/src/chat/svg-figure.d.ts +33 -0
- package/src/chat/svg-figure.ts +399 -0
- package/src/chat/svg-source.d.ts +49 -0
- package/src/chat/svg-source.ts +188 -0
- package/src/chat/table-chart.d.ts +89 -0
- package/src/chat/table-chart.ts +294 -0
- package/src/chat/tool-execution.d.ts +2 -0
- package/src/chat/tool-execution.ts +80 -6
- package/src/chat/user-message.d.ts +15 -2
- package/src/chat/user-message.ts +55 -3
- package/src/chrome/shared.ts +53 -0
- package/src/chrome/transcript-container.d.ts +6 -0
- package/src/chrome/transcript-container.ts +93 -13
- package/src/components/box.d.ts +1 -0
- package/src/components/box.ts +5 -0
- package/src/components/disclosure.d.ts +1 -0
- package/src/components/disclosure.ts +8 -0
- package/src/components/editor.d.ts +15 -4
- package/src/components/editor.ts +294 -46
- package/src/components/form.d.ts +1 -0
- package/src/components/form.ts +8 -0
- package/src/components/image.d.ts +12 -44
- package/src/components/image.ts +47 -151
- package/src/components/key-value-list.d.ts +1 -0
- package/src/components/key-value-list.ts +4 -0
- package/src/components/layout/row.d.ts +1 -0
- package/src/components/layout/row.ts +7 -0
- package/src/components/layout/stack.d.ts +1 -0
- package/src/components/layout/stack.ts +7 -0
- package/src/components/markdown.d.ts +36 -3
- package/src/components/markdown.ts +433 -318
- package/src/components/scroll-view.d.ts +1 -0
- package/src/components/scroll-view.ts +5 -0
- package/src/components/section.d.ts +1 -0
- package/src/components/section.ts +5 -0
- package/src/components/settings-list.ts +2 -0
- package/src/components/table.d.ts +1 -0
- package/src/components/table.ts +4 -0
- package/src/components/text.d.ts +1 -0
- package/src/components/text.ts +7 -0
- package/src/components/tree-view.d.ts +7 -0
- package/src/components/tree-view.ts +38 -8
- package/src/components/truncated-text.d.ts +1 -0
- package/src/components/truncated-text.ts +5 -0
- package/src/desktop-notify.ts +1 -1
- package/src/fuzzy.d.ts +29 -1
- package/src/fuzzy.ts +115 -30
- package/src/key-hint-format.d.ts +1 -1
- package/src/key-hint-format.ts +1 -1
- package/src/keybinding-matchers.d.ts +3 -0
- package/src/keybinding-matchers.ts +6 -2
- package/src/keybindings.d.ts +1 -1
- package/src/keybindings.ts +1 -1
- package/src/kitty-graphics.d.ts +2 -2
- package/src/kitty-graphics.ts +4 -4
- package/src/loop-watchdog.d.ts +8 -7
- package/src/loop-watchdog.ts +18 -13
- package/src/multiplexers/cmux.d.ts +16 -0
- package/src/multiplexers/cmux.ts +29 -0
- package/src/multiplexers/herdr.d.ts +26 -0
- package/src/multiplexers/herdr.ts +64 -0
- package/src/multiplexers/notify.d.ts +13 -0
- package/src/multiplexers/notify.ts +24 -0
- package/src/multiplexers/orca.d.ts +12 -0
- package/src/multiplexers/orca.ts +16 -0
- package/src/multiplexers/screen.d.ts +9 -0
- package/src/multiplexers/screen.ts +15 -0
- package/src/multiplexers/tmux.d.ts +29 -0
- package/src/multiplexers/tmux.ts +40 -0
- package/src/multiplexers/types.d.ts +100 -0
- package/src/multiplexers/types.ts +110 -0
- package/src/multiplexers/wmux.d.ts +12 -0
- package/src/multiplexers/wmux.ts +16 -0
- package/src/multiplexers/zellij.d.ts +12 -0
- package/src/multiplexers/zellij.ts +24 -0
- package/src/native/backend.d.ts +3 -2
- package/src/native/backend.ts +174 -18
- package/src/native/blobs.d.ts +7 -3
- package/src/native/blobs.ts +47 -12
- package/src/native/encode.d.ts +3 -4
- package/src/native/encode.ts +58 -31
- package/src/native/overlay.d.ts +5 -0
- package/src/native/overlay.ts +13 -0
- package/src/native/reconcile.ts +4 -0
- package/src/overlays/agent-hub-projection.d.ts +6 -5
- package/src/overlays/agent-hub-projection.ts +124 -13
- package/src/overlays/agent-hub-types.d.ts +9 -0
- package/src/overlays/agent-hub-types.ts +6 -0
- package/src/overlays/agent-hub.ts +44 -29
- package/src/overlays/agent-transcript-viewer.ts +91 -23
- package/src/overlays/agents-hub.ts +28 -8
- package/src/overlays/annotation-overlay.ts +293 -90
- package/src/overlays/annotation-types.ts +2 -0
- package/src/overlays/ask-dialog.ts +7 -10
- package/src/overlays/btw-history-panel.ts +12 -1
- package/src/overlays/copy-selector.ts +30 -8
- package/src/overlays/extensions/state-manager.ts +52 -12
- package/src/overlays/history-search.ts +100 -7
- package/src/overlays/hook-selector.ts +12 -5
- package/src/overlays/jobs-panel.ts +28 -46
- package/src/overlays/model-browser.ts +341 -110
- package/src/overlays/model-hub.ts +300 -49
- package/src/overlays/move-overlay.ts +28 -14
- package/src/overlays/oauth-selector.ts +54 -70
- package/src/overlays/plan-review-overlay.ts +194 -70
- package/src/overlays/plugin-settings.ts +3 -3
- package/src/overlays/report-panel.ts +2 -2
- package/src/overlays/rewind-selector.ts +86 -17
- package/src/overlays/session-observer-registry.d.ts +2 -0
- package/src/overlays/session-observer-registry.ts +10 -2
- package/src/overlays/session-selector.ts +64 -26
- package/src/overlays/settings-defs.ts +4 -3
- package/src/overlays/settings-selector.ts +33 -8
- package/src/overlays/snapcompact-shape-preview.ts +2 -3
- package/src/overlays/stats-notice.ts +1 -1
- package/src/overlays/tree-selector.ts +41 -20
- package/src/overlays/usage-dashboard.ts +100 -50
- package/src/overlays/usage-display.ts +10 -4
- package/src/prompt/attachment-chips.ts +15 -11
- package/src/prompt/composer-attachments.ts +6 -4
- package/src/prompt/composer.ts +4 -15
- package/src/prompt/custom-editor.ts +54 -26
- package/src/prompt/image-source.ts +2 -1
- package/src/prompt/interactive-context-helpers.ts +30 -18
- package/src/prompt/macos-spelling.ts +7 -0
- package/src/prompt/magic-keywords.ts +11 -2
- package/src/prompt/model-mention-autocomplete.ts +14 -14
- package/src/prompt/tips.txt +6 -5
- package/src/prompt/welcome.ts +55 -12
- package/src/prompt/word-completion.ts +38 -5
- package/src/render/composite.d.ts +2 -0
- package/src/render/composite.ts +64 -0
- package/src/render/output-pane.d.ts +1 -0
- package/src/render/output-pane.ts +6 -0
- package/src/render/render-utils.d.ts +1 -0
- package/src/render/render-utils.ts +11 -0
- package/src/render/tool-card.d.ts +6 -0
- package/src/render/tool-card.ts +15 -0
- package/src/render/width-aware-text.d.ts +2 -0
- package/src/render/width-aware-text.ts +5 -0
- package/src/setup/scenes/model.ts +2 -1
- package/src/setup/scenes/types.ts +1 -0
- package/src/status-line/component.ts +292 -78
- package/src/status-line/host.ts +4 -1
- package/src/status-line/segments.ts +40 -22
- package/src/status-line/types.ts +15 -3
- package/src/terminal-capabilities.d.ts +27 -35
- package/src/terminal-capabilities.ts +75 -189
- package/src/terminal-multiplexer.d.ts +127 -14
- package/src/terminal-multiplexer.ts +112 -33
- package/src/terminal.d.ts +22 -6
- package/src/terminal.ts +139 -37
- package/src/theme/active-symbols.d.ts +1 -1
- package/src/theme/active-symbols.ts +1 -1
- package/src/theme/schema-validation.ts +1 -0
- package/src/theme/schema.d.ts +11 -2
- package/src/theme/schema.ts +14 -1
- package/src/theme/session-color.ts +1 -7
- package/src/theme/symbols.d.ts +1 -1
- package/src/theme/symbols.ts +6 -1
- package/src/theme/theme-class.d.ts +7 -0
- package/src/theme/theme-class.ts +26 -1
- package/src/theme/theme-schema.json +5 -1
- package/src/theme/theme.d.ts +1 -1
- package/src/theme/theme.ts +8 -4
- package/src/theme/tui-adapters.d.ts +3 -0
- package/src/theme/tui-adapters.ts +68 -0
- package/src/tmux.d.ts +11 -4
- package/src/tmux.ts +47 -11
- package/src/tools/ast-edit.ts +59 -35
- package/src/tools/ast-grep.ts +35 -16
- package/src/tools/bash.ts +92 -42
- package/src/tools/daemon.d.ts +1 -1
- package/src/tools/daemon.ts +1 -1
- package/src/tools/default-renderer.ts +35 -19
- package/src/tools/edit.ts +142 -120
- package/src/tools/eval-format/index.ts +28 -2
- package/src/tools/eval-format/javascript.ts +7 -5
- package/src/tools/eval.d.ts +22 -3
- package/src/tools/eval.ts +364 -208
- package/src/tools/fetch.ts +5 -6
- package/src/tools/glob.d.ts +4 -0
- package/src/tools/glob.ts +33 -65
- package/src/tools/grep.ts +81 -48
- package/src/tools/index.ts +2 -0
- package/src/tools/lsp.ts +81 -59
- package/src/tools/mcp.ts +111 -68
- package/src/tools/native-view.d.ts +47 -0
- package/src/tools/native-view.ts +102 -1
- package/src/tools/output-meta.d.ts +11 -1
- package/src/tools/output-meta.ts +23 -3
- package/src/tools/read.ts +7 -5
- package/src/tools/renderer.d.ts +37 -2
- package/src/tools/renderer.ts +42 -2
- package/src/tools/streaming-output.d.ts +20 -1
- package/src/tools/streaming-output.ts +149 -50
- package/src/tools/task.d.ts +0 -4
- package/src/tools/task.ts +112 -59
- package/src/tools/todo.ts +119 -30
- package/src/tools/wait.ts +34 -24
- package/src/tools/web-search.ts +1 -1
- package/src/tools/write.d.ts +9 -2
- package/src/tools/write.ts +147 -95
- package/src/tools/yield.d.ts +22 -0
- package/src/tools/yield.ts +225 -0
- package/src/tui.d.ts +31 -0
- package/src/tui.ts +309 -160
- package/src/utils.d.ts +6 -0
- package/src/utils.ts +19 -0
- package/src/vim.d.ts +19 -5
- package/src/vim.ts +333 -18
- package/src/windows-input-mode.ts +8 -0
|
@@ -13,13 +13,15 @@
|
|
|
13
13
|
* Focus regions (`toc`/`body`/`actions`) cycle with Tab/Shift+Tab; arrows move
|
|
14
14
|
* within the focused region and step left into the sidebar. The default focus is
|
|
15
15
|
* `actions`, so the muscle memory of the old single-target overlay carries over:
|
|
16
|
-
* ↑/↓ select options, Enter confirms, ←/→ drives the slider
|
|
17
|
-
*
|
|
16
|
+
* ↑/↓ select options, Enter confirms, ←/→ drives the slider, g/G + PgUp/PgDn
|
|
17
|
+
* scroll, and the external-editor key opens the plan. Under Tern the options
|
|
18
|
+
* are a horizontal bar, so ←/→ select options and Shift+←/→ drive the slider.
|
|
18
19
|
*/
|
|
19
20
|
import {
|
|
20
21
|
type Component,
|
|
21
22
|
Ellipsis,
|
|
22
23
|
Editor,
|
|
24
|
+
getWidthConfigEpoch,
|
|
23
25
|
Markdown,
|
|
24
26
|
type MarkdownTheme,
|
|
25
27
|
matchesKey,
|
|
@@ -31,7 +33,7 @@ import {
|
|
|
31
33
|
} from "../index";
|
|
32
34
|
import { sanitizeText } from "@linxiraos/pi-utils";
|
|
33
35
|
import { sanitizeStatusText } from "../chrome/shared";
|
|
34
|
-
import { getEditorTheme, getMarkdownTheme, theme } from "../theme/theme";
|
|
36
|
+
import { getEditorTheme, getMarkdownTheme, getThemeEpoch, theme } from "../theme/theme";
|
|
35
37
|
import {
|
|
36
38
|
matchesAppExternalEditor,
|
|
37
39
|
matchesSelectCancel,
|
|
@@ -58,6 +60,7 @@ import type { KeyName } from "../key-hint-format";
|
|
|
58
60
|
import { col, item, keyed, md, node, row as rowNode, span, text } from "../native/describe";
|
|
59
61
|
import { leafKey, type NativeChild, type NativeNode, type NativeUiEvent } from "../native/node";
|
|
60
62
|
import { actionButton, actionHint, hintsRow, itemIndex, type NativeHint, selectList } from "../native/overlay";
|
|
63
|
+
import { isNativeRendering } from "../native/state";
|
|
61
64
|
import { getKeybindings } from "../keybindings";
|
|
62
65
|
|
|
63
66
|
/** Title shown in the overlay's top border. */
|
|
@@ -74,6 +77,8 @@ const SIDEBAR_MIN_TOTAL_WIDTH = 64;
|
|
|
74
77
|
const SIDEBAR_MIN_BODY_WIDTH = 40;
|
|
75
78
|
/** Persisted line-context cap; render-time captions clamp again to the viewport. */
|
|
76
79
|
const MAX_ANNOTATION_CONTEXT_WIDTH = 120;
|
|
80
|
+
/** Oldest delete/annotate undo snapshots are dropped past this depth (each holds the full plan text). */
|
|
81
|
+
const MAX_UNDO_ENTRIES = 100;
|
|
77
82
|
|
|
78
83
|
type Focus = "toc" | "body" | "actions";
|
|
79
84
|
|
|
@@ -146,6 +151,78 @@ interface UndoEntry {
|
|
|
146
151
|
deleted: string[];
|
|
147
152
|
}
|
|
148
153
|
|
|
154
|
+
/** Concatenated plan body rows and their anchors, reused while every render input is unchanged. */
|
|
155
|
+
interface BodyLayout {
|
|
156
|
+
width: number;
|
|
157
|
+
annotationRev: number;
|
|
158
|
+
themeEpoch: number;
|
|
159
|
+
widthEpoch: number;
|
|
160
|
+
/** Each section's `Markdown.render` result the layout was built from (compared by identity). */
|
|
161
|
+
rendered: readonly (readonly string[])[];
|
|
162
|
+
lines: readonly string[];
|
|
163
|
+
anchors: BodyRowAnchor[];
|
|
164
|
+
offsets: number[];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Line contexts per rendered-lines array; `Markdown.render` returns a stable array per (text, width). */
|
|
168
|
+
const lineContextCache = new WeakMap<readonly string[], readonly LineAnchorContext[]>();
|
|
169
|
+
/** Match keys per context list, so resolving several annotations normalizes each row once. */
|
|
170
|
+
const normalizedContextCache = new WeakMap<readonly LineAnchorContext[], readonly string[]>();
|
|
171
|
+
|
|
172
|
+
function lineContexts(rendered: readonly string[]): readonly LineAnchorContext[] {
|
|
173
|
+
let contexts = lineContextCache.get(rendered);
|
|
174
|
+
if (!contexts) {
|
|
175
|
+
contexts = rendered.map(line => {
|
|
176
|
+
const sanitized = sanitizeStatusText(line);
|
|
177
|
+
const truncated = visibleWidth(sanitized) > MAX_ANNOTATION_CONTEXT_WIDTH;
|
|
178
|
+
const text = truncateToWidth(sanitized, MAX_ANNOTATION_CONTEXT_WIDTH, Ellipsis.Unicode);
|
|
179
|
+
return { text: text || "(blank line)", truncated };
|
|
180
|
+
});
|
|
181
|
+
lineContextCache.set(rendered, contexts);
|
|
182
|
+
}
|
|
183
|
+
return contexts;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function normalizeLineContext(context: LineAnchorContext): string {
|
|
187
|
+
const normalized = sanitizeStatusText(context.text).replace(/\s+/g, " ").trim();
|
|
188
|
+
return context.truncated && normalized.endsWith("…") ? normalized.slice(0, -1) : normalized;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Row in `contexts` nearest `storedRow` whose text matches the stored context, or -1. */
|
|
192
|
+
function resolveLineRow(
|
|
193
|
+
storedRow: number,
|
|
194
|
+
storedContext: LineAnchorContext,
|
|
195
|
+
contexts: readonly LineAnchorContext[],
|
|
196
|
+
): number {
|
|
197
|
+
if (contexts.length === 0) return -1;
|
|
198
|
+
const targetRow = Math.max(0, Math.floor(storedRow));
|
|
199
|
+
const normalizedStoredContext = normalizeLineContext(storedContext);
|
|
200
|
+
if (!normalizedStoredContext) return -1;
|
|
201
|
+
let normalizedContexts = normalizedContextCache.get(contexts);
|
|
202
|
+
if (!normalizedContexts) {
|
|
203
|
+
normalizedContexts = contexts.map(normalizeLineContext);
|
|
204
|
+
normalizedContextCache.set(contexts, normalizedContexts);
|
|
205
|
+
}
|
|
206
|
+
let best = -1;
|
|
207
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
208
|
+
for (let row = 0; row < normalizedContexts.length; row++) {
|
|
209
|
+
const normalizedContext = normalizedContexts[row]!;
|
|
210
|
+
if (
|
|
211
|
+
normalizedContext !== normalizedStoredContext &&
|
|
212
|
+
!normalizedContext.includes(normalizedStoredContext) &&
|
|
213
|
+
!normalizedStoredContext.includes(normalizedContext)
|
|
214
|
+
) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const distance = Math.abs(row - targetRow);
|
|
218
|
+
if (distance < bestDistance) {
|
|
219
|
+
best = row;
|
|
220
|
+
bestDistance = distance;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return best;
|
|
224
|
+
}
|
|
225
|
+
|
|
149
226
|
export interface PlanReviewOverlayCallbacks {
|
|
150
227
|
/** Invoked with the chosen option label (never a disabled one). */
|
|
151
228
|
onPick: (label: string) => void;
|
|
@@ -197,6 +274,10 @@ export class PlanReviewOverlay implements Component {
|
|
|
197
274
|
#sectionOffsets: number[] = [];
|
|
198
275
|
/** Rendered body row to underlying plan row; callouts retain their owner's anchor. */
|
|
199
276
|
#bodyRowAnchors: BodyRowAnchor[] = [];
|
|
277
|
+
/** Memoized {@link #buildBody} result. */
|
|
278
|
+
#bodyLayout: BodyLayout | undefined;
|
|
279
|
+
/** Lines last handed to the scroll view; unchanged layouts skip the copy and keep its render cache. */
|
|
280
|
+
#scrollLines: readonly string[] | undefined;
|
|
200
281
|
#undo: UndoEntry[] = [];
|
|
201
282
|
/** Titles of sections deleted in the overlay, surfaced as Refine feedback. */
|
|
202
283
|
#deleted: string[] = [];
|
|
@@ -442,8 +523,8 @@ export class PlanReviewOverlay implements Component {
|
|
|
442
523
|
) {
|
|
443
524
|
continue;
|
|
444
525
|
}
|
|
445
|
-
const contexts = section.md.render(MAX_ANNOTATION_CONTEXT_WIDTH)
|
|
446
|
-
const row =
|
|
526
|
+
const contexts = lineContexts(section.md.render(MAX_ANNOTATION_CONTEXT_WIDTH));
|
|
527
|
+
const row = resolveLineRow(
|
|
447
528
|
entry.target.row,
|
|
448
529
|
{ text: entry.target.context, truncated: entry.target.contextTruncated === true },
|
|
449
530
|
contexts,
|
|
@@ -666,6 +747,10 @@ export class PlanReviewOverlay implements Component {
|
|
|
666
747
|
}
|
|
667
748
|
|
|
668
749
|
#handleActions(data: string): void {
|
|
750
|
+
if (isNativeRendering()) {
|
|
751
|
+
this.#handleNativeActions(data);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
669
754
|
// Left/right always drive the slider. The sidebar sits beside the body
|
|
670
755
|
// (above this row), not the slider, so stealing left for it would strand
|
|
671
756
|
// the operator unable to step the model tier back — reach the ToC via Tab.
|
|
@@ -695,6 +780,37 @@ export class PlanReviewOverlay implements Component {
|
|
|
695
780
|
this.#handleBodyScroll(data);
|
|
696
781
|
}
|
|
697
782
|
|
|
783
|
+
/** Tern lays the options out as a horizontal decision bar: ←/→ step through
|
|
784
|
+
* it, ↑ returns to the body above, and the model slider moves to Shift+←/→. */
|
|
785
|
+
#handleNativeActions(data: string): void {
|
|
786
|
+
if (matchesKey(data, "shift+left")) {
|
|
787
|
+
this.#moveSlider(-1);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
if (matchesKey(data, "shift+right")) {
|
|
791
|
+
this.#moveSlider(1);
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
if (matchesKey(data, "left") || matchesKey(data, "h")) {
|
|
795
|
+
this.#moveSelection(-1);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
if (matchesKey(data, "right") || matchesKey(data, "l")) {
|
|
799
|
+
this.#moveSelection(1);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
if (matchesSelectUp(data) || matchesKey(data, "k")) {
|
|
803
|
+
this.#setFocus("body");
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
if (matchesSelectDown(data) || matchesKey(data, "j")) return;
|
|
807
|
+
if (matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n") {
|
|
808
|
+
this.#confirmSelection();
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
this.#handleBodyScroll(data);
|
|
812
|
+
}
|
|
813
|
+
|
|
698
814
|
#handleBody(data: string): void {
|
|
699
815
|
if (data === "a") {
|
|
700
816
|
this.#startBodyAnnotate();
|
|
@@ -829,6 +945,7 @@ export class PlanReviewOverlay implements Component {
|
|
|
829
945
|
}
|
|
830
946
|
|
|
831
947
|
#pushUndo(): void {
|
|
948
|
+
if (this.#undo.length >= MAX_UNDO_ENTRIES) this.#undo.shift();
|
|
832
949
|
this.#undo.push({
|
|
833
950
|
text: joinPlanSections(this.#sections),
|
|
834
951
|
annotations: this.#sections.map(section =>
|
|
@@ -922,7 +1039,7 @@ export class PlanReviewOverlay implements Component {
|
|
|
922
1039
|
seenRows.add(candidate.row);
|
|
923
1040
|
sectionAnchors.push({ text: candidate.context, truncated: candidate.contextTruncated });
|
|
924
1041
|
}
|
|
925
|
-
const resolvedRow =
|
|
1042
|
+
const resolvedRow = resolveLineRow(
|
|
926
1043
|
annotation.target.row,
|
|
927
1044
|
{ text: annotation.target.context, truncated: annotation.target.contextTruncated },
|
|
928
1045
|
sectionAnchors,
|
|
@@ -1150,23 +1267,64 @@ export class PlanReviewOverlay implements Component {
|
|
|
1150
1267
|
|
|
1151
1268
|
#layoutBody(lines: readonly string[], height: number): void {
|
|
1152
1269
|
this.#captureScrollProgress();
|
|
1153
|
-
this.#
|
|
1270
|
+
if (lines !== this.#scrollLines) {
|
|
1271
|
+
this.#scrollView.setLines(lines);
|
|
1272
|
+
this.#scrollLines = lines;
|
|
1273
|
+
}
|
|
1154
1274
|
this.#scrollView.setHeight(height);
|
|
1155
1275
|
const maxOffset = this.#scrollView.getMaxScrollOffset();
|
|
1156
1276
|
if (maxOffset > 0) this.#scrollView.setScrollOffset(Math.round(this.#scrollProgress * maxOffset));
|
|
1157
1277
|
}
|
|
1158
1278
|
|
|
1159
|
-
/**
|
|
1160
|
-
|
|
1279
|
+
/**
|
|
1280
|
+
* Build the concatenated body lines and map each rendered row back to a plan
|
|
1281
|
+
* anchor. Memoized on width, annotation revision, theme/width epochs and each
|
|
1282
|
+
* section's rendered-lines identity, so steady frames and scrolling reuse it.
|
|
1283
|
+
*/
|
|
1284
|
+
#buildBody(bodyContentWidth: number): readonly string[] {
|
|
1285
|
+
const themeEpoch = getThemeEpoch();
|
|
1286
|
+
const widthEpoch = getWidthConfigEpoch();
|
|
1287
|
+
const cached = this.#bodyLayout;
|
|
1288
|
+
if (
|
|
1289
|
+
cached?.width === bodyContentWidth &&
|
|
1290
|
+
cached.annotationRev === this.#annotationRev &&
|
|
1291
|
+
cached.themeEpoch === themeEpoch &&
|
|
1292
|
+
cached.widthEpoch === widthEpoch &&
|
|
1293
|
+
cached.rendered.length === this.#sections.length &&
|
|
1294
|
+
this.#sections.every((section, i) => section.md.render(bodyContentWidth) === cached.rendered[i])
|
|
1295
|
+
) {
|
|
1296
|
+
this.#sectionOffsets = cached.offsets;
|
|
1297
|
+
this.#bodyRowAnchors = cached.anchors;
|
|
1298
|
+
return cached.lines;
|
|
1299
|
+
}
|
|
1161
1300
|
const lines: string[] = [];
|
|
1162
1301
|
const anchors: BodyRowAnchor[] = [];
|
|
1302
|
+
const renderedSections: (readonly string[])[] = [];
|
|
1163
1303
|
// oxlint-disable-next-line unicorn/no-new-array -- length preallocation
|
|
1164
1304
|
const offsets: number[] = new Array(this.#sections.length);
|
|
1165
1305
|
for (let sectionIndex = 0; sectionIndex < this.#sections.length; sectionIndex++) {
|
|
1166
1306
|
const section = this.#sections[sectionIndex]!;
|
|
1167
1307
|
offsets[sectionIndex] = lines.length;
|
|
1168
1308
|
const rendered = section.md.render(bodyContentWidth);
|
|
1169
|
-
|
|
1309
|
+
renderedSections.push(rendered);
|
|
1310
|
+
const contexts = lineContexts(rendered);
|
|
1311
|
+
// Resolve each note's row once; notes on one row keep their array order.
|
|
1312
|
+
let notesByRow: Map<number, string[]> | undefined;
|
|
1313
|
+
for (const annotation of section.annotations) {
|
|
1314
|
+
const annotationRow =
|
|
1315
|
+
annotation.target.kind === "section"
|
|
1316
|
+
? 0
|
|
1317
|
+
: resolveLineRow(
|
|
1318
|
+
annotation.target.row,
|
|
1319
|
+
{ text: annotation.target.context, truncated: annotation.target.contextTruncated },
|
|
1320
|
+
contexts,
|
|
1321
|
+
);
|
|
1322
|
+
if (annotationRow < 0) continue;
|
|
1323
|
+
notesByRow ??= new Map();
|
|
1324
|
+
const notes = notesByRow.get(annotationRow);
|
|
1325
|
+
if (notes) notes.push(annotation.note);
|
|
1326
|
+
else notesByRow.set(annotationRow, [annotation.note]);
|
|
1327
|
+
}
|
|
1170
1328
|
for (let row = 0; row < rendered.length; row++) {
|
|
1171
1329
|
const context = contexts[row]!;
|
|
1172
1330
|
const anchor = {
|
|
@@ -1177,67 +1335,24 @@ export class PlanReviewOverlay implements Component {
|
|
|
1177
1335
|
};
|
|
1178
1336
|
lines.push(rendered[row]!);
|
|
1179
1337
|
anchors.push(anchor);
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
? 0
|
|
1184
|
-
: this.#resolveLineRow(
|
|
1185
|
-
annotation.target.row,
|
|
1186
|
-
{
|
|
1187
|
-
text: annotation.target.context,
|
|
1188
|
-
truncated: annotation.target.contextTruncated,
|
|
1189
|
-
},
|
|
1190
|
-
contexts,
|
|
1191
|
-
);
|
|
1192
|
-
if (annotationRow === row) {
|
|
1193
|
-
this.#appendAnnotationCallout(lines, anchors, annotation.note, anchor, bodyContentWidth);
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1338
|
+
const notes = notesByRow?.get(row);
|
|
1339
|
+
if (!notes) continue;
|
|
1340
|
+
for (const note of notes) this.#appendAnnotationCallout(lines, anchors, note, anchor, bodyContentWidth);
|
|
1196
1341
|
}
|
|
1197
1342
|
}
|
|
1198
1343
|
this.#sectionOffsets = offsets;
|
|
1199
1344
|
this.#bodyRowAnchors = anchors;
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
#resolveLineRow(
|
|
1211
|
-
storedRow: number,
|
|
1212
|
-
storedContext: LineAnchorContext,
|
|
1213
|
-
contexts: readonly LineAnchorContext[],
|
|
1214
|
-
): number {
|
|
1215
|
-
if (contexts.length === 0) return -1;
|
|
1216
|
-
const targetRow = Math.max(0, Math.floor(storedRow));
|
|
1217
|
-
const normalize = (context: LineAnchorContext): string => {
|
|
1218
|
-
const normalized = sanitizeStatusText(context.text).replace(/\s+/g, " ").trim();
|
|
1219
|
-
return context.truncated && normalized.endsWith("…") ? normalized.slice(0, -1) : normalized;
|
|
1345
|
+
this.#bodyLayout = {
|
|
1346
|
+
width: bodyContentWidth,
|
|
1347
|
+
annotationRev: this.#annotationRev,
|
|
1348
|
+
themeEpoch,
|
|
1349
|
+
widthEpoch,
|
|
1350
|
+
rendered: renderedSections,
|
|
1351
|
+
lines,
|
|
1352
|
+
anchors,
|
|
1353
|
+
offsets,
|
|
1220
1354
|
};
|
|
1221
|
-
|
|
1222
|
-
if (!normalizedStoredContext) return -1;
|
|
1223
|
-
let best = -1;
|
|
1224
|
-
let bestDistance = Number.POSITIVE_INFINITY;
|
|
1225
|
-
for (let row = 0; row < contexts.length; row++) {
|
|
1226
|
-
const normalizedContext = normalize(contexts[row]!);
|
|
1227
|
-
if (
|
|
1228
|
-
normalizedContext !== normalizedStoredContext &&
|
|
1229
|
-
!normalizedContext.includes(normalizedStoredContext) &&
|
|
1230
|
-
!normalizedStoredContext.includes(normalizedContext)
|
|
1231
|
-
) {
|
|
1232
|
-
continue;
|
|
1233
|
-
}
|
|
1234
|
-
const distance = Math.abs(row - targetRow);
|
|
1235
|
-
if (distance < bestDistance) {
|
|
1236
|
-
best = row;
|
|
1237
|
-
bestDistance = distance;
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
return best;
|
|
1355
|
+
return lines;
|
|
1241
1356
|
}
|
|
1242
1357
|
|
|
1243
1358
|
#appendAnnotationCallout(
|
|
@@ -1492,7 +1607,7 @@ export class PlanReviewOverlay implements Component {
|
|
|
1492
1607
|
return this.#nativeRoot;
|
|
1493
1608
|
}
|
|
1494
1609
|
|
|
1495
|
-
/** Head tools: Copy `c
|
|
1610
|
+
/** Head tools: Copy `c`, the external editor and Cancel, mirroring their keys. */
|
|
1496
1611
|
#describeTools(): NativeNode | undefined {
|
|
1497
1612
|
if (this.#committed) return undefined;
|
|
1498
1613
|
const buttons: NativeNode[] = [];
|
|
@@ -1503,6 +1618,12 @@ export class PlanReviewOverlay implements Component {
|
|
|
1503
1618
|
if (this.callbacks.onExternalEditor) {
|
|
1504
1619
|
buttons.push(actionButton("Edit in $EDITOR", "externalEditor", editorKeyId ? { keys: editorKeyId } : {}));
|
|
1505
1620
|
}
|
|
1621
|
+
// Cancel the review the way the cancel key does; while annotating that key
|
|
1622
|
+
// only leaves the draft/chooser, so the button is hidden there.
|
|
1623
|
+
if (!this.#annotating && !this.#annotationChooser) {
|
|
1624
|
+
const cancelKeyId = getKeybindings().getKeys("tui.select.cancel")[0];
|
|
1625
|
+
buttons.push(actionButton("Cancel", "cancel", cancelKeyId ? { keys: cancelKeyId } : {}));
|
|
1626
|
+
}
|
|
1506
1627
|
if (buttons.length === 0) return undefined;
|
|
1507
1628
|
return node("row", { role: "zeta.plan.tools", gap: "sm", align: "center", justify: "end" }, buttons, "tools");
|
|
1508
1629
|
}
|
|
@@ -1510,12 +1631,15 @@ export class PlanReviewOverlay implements Component {
|
|
|
1510
1631
|
handleNativeEvent(event: NativeUiEvent): void {
|
|
1511
1632
|
if (this.#committed) return;
|
|
1512
1633
|
if (event.type === "action") {
|
|
1513
|
-
// Head tools run exactly what `c
|
|
1634
|
+
// Head tools run exactly what `c`, the external-editor key and Esc run.
|
|
1514
1635
|
if (this.#annotating || this.#annotationChooser) return;
|
|
1515
1636
|
if (event.act === "copyPlan" && this.callbacks.onCopyPlan) {
|
|
1516
1637
|
void this.callbacks.onCopyPlan(joinPlanSections(this.#sections));
|
|
1517
1638
|
} else if (event.act === "externalEditor") {
|
|
1518
1639
|
this.callbacks.onExternalEditor?.();
|
|
1640
|
+
} else if (event.act === "cancel") {
|
|
1641
|
+
this.#committed = true;
|
|
1642
|
+
this.callbacks.onCancel();
|
|
1519
1643
|
}
|
|
1520
1644
|
return;
|
|
1521
1645
|
}
|
|
@@ -1686,8 +1810,8 @@ export class PlanReviewOverlay implements Component {
|
|
|
1686
1810
|
const hints: (NativeHint | undefined)[] = [];
|
|
1687
1811
|
switch (this.#focus) {
|
|
1688
1812
|
case "actions":
|
|
1689
|
-
hints.push(
|
|
1690
|
-
if (this.#slider) hints.push(key(["left", "right"], "model"));
|
|
1813
|
+
hints.push(key(["left", "right"], "select"), key("enter", "confirm"));
|
|
1814
|
+
if (this.#slider) hints.push(key(["shift+left", "shift+right"], "model"));
|
|
1691
1815
|
break;
|
|
1692
1816
|
case "toc":
|
|
1693
1817
|
hints.push(
|
|
@@ -378,7 +378,7 @@ export class PluginListComponent extends OverlayPanel {
|
|
|
378
378
|
lead:
|
|
379
379
|
rows.length > 0
|
|
380
380
|
? "Plugins installed for you and this project. Configure one to turn it or its features on and off."
|
|
381
|
-
: "No plugins installed. Install one with
|
|
381
|
+
: "No plugins installed. Install one with zetacode plugin install <package>, or <name>@<marketplace>.",
|
|
382
382
|
sections: rows.length > 0 ? [{ id: "installed", title: "Installed", rows }] : [],
|
|
383
383
|
focus: this.#selectList.getSelectedItem()?.value ?? null,
|
|
384
384
|
editing: null,
|
|
@@ -403,10 +403,10 @@ export class PluginListComponent extends OverlayPanel {
|
|
|
403
403
|
text([span("No plugins installed", "muted")]),
|
|
404
404
|
node("kv", {
|
|
405
405
|
items: [
|
|
406
|
-
{ k: "Install npm plugins", v: [span("
|
|
406
|
+
{ k: "Install npm plugins", v: [span("zetacode plugin install <package>", "code")] },
|
|
407
407
|
{
|
|
408
408
|
k: "Install marketplace plugins",
|
|
409
|
-
v: [span("
|
|
409
|
+
v: [span("zetacode plugin install <name>@<marketplace>", "code")],
|
|
410
410
|
},
|
|
411
411
|
],
|
|
412
412
|
}),
|
|
@@ -72,9 +72,9 @@ export class ReportPanel extends OverlayPanel {
|
|
|
72
72
|
#native: { scroll: NativeScroll | undefined; node: NativeNode } | undefined;
|
|
73
73
|
|
|
74
74
|
constructor(options: ReportPanelOptions) {
|
|
75
|
-
super(options.title, "
|
|
75
|
+
super(options.title, "zeta.overlay.report");
|
|
76
76
|
this.nativeOverlay = {
|
|
77
|
-
role: "
|
|
77
|
+
role: "zeta.overlay.report",
|
|
78
78
|
size: "lg",
|
|
79
79
|
anchor: "center",
|
|
80
80
|
head: options.head ?? options.title,
|
|
@@ -158,6 +158,14 @@ const STRIP_GAP = 2;
|
|
|
158
158
|
/** Duration of the branch-swap camera slide. */
|
|
159
159
|
const SLIDE_MS = 160;
|
|
160
160
|
|
|
161
|
+
/** Same length and identical elements (or both undefined). */
|
|
162
|
+
function sameElements<T>(a: readonly T[] | undefined, b: readonly T[] | undefined): boolean {
|
|
163
|
+
if (a === b) return true;
|
|
164
|
+
if (a === undefined || b === undefined || a.length !== b.length) return false;
|
|
165
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
161
169
|
export class RewindSelectorComponent implements Component {
|
|
162
170
|
#builder: ChatTranscriptBuilder;
|
|
163
171
|
#browser: TranscriptBrowser;
|
|
@@ -196,6 +204,24 @@ export class RewindSelectorComponent implements Component {
|
|
|
196
204
|
#rowText = new WeakMap<readonly string[], string>();
|
|
197
205
|
/** Native filter haystack: lowercased turn text per main target (no rendered rows under TSP). */
|
|
198
206
|
#nativeTexts: { targets: OutlineTarget[]; texts: string[] } | undefined;
|
|
207
|
+
/** Compiled word patterns of the last filter query. */
|
|
208
|
+
#filterPatterns: { query: string; patterns: RegExp[] } | undefined;
|
|
209
|
+
/**
|
|
210
|
+
* Last {@link #filterMatches} result and its inputs. Frames hand back fresh
|
|
211
|
+
* outer `mainRows`/`mainVisible` arrays over cached per-child rows, so those
|
|
212
|
+
* compare element-wise.
|
|
213
|
+
*/
|
|
214
|
+
#filterMemo:
|
|
215
|
+
| {
|
|
216
|
+
query: string;
|
|
217
|
+
targets: OutlineTarget[];
|
|
218
|
+
expanded: boolean;
|
|
219
|
+
native: boolean;
|
|
220
|
+
mainRows: readonly (readonly string[])[];
|
|
221
|
+
mainVisible: readonly boolean[] | undefined;
|
|
222
|
+
matches: readonly number[];
|
|
223
|
+
}
|
|
224
|
+
| undefined;
|
|
199
225
|
/** Last described bar and the state it was built from. */
|
|
200
226
|
#bar: { memo: string; targets: OutlineTarget[]; node: NativeNode } | undefined;
|
|
201
227
|
|
|
@@ -529,14 +555,25 @@ export class RewindSelectorComponent implements Component {
|
|
|
529
555
|
* Matching the rendered rows keeps results honest: collapsed tool output
|
|
530
556
|
* only matches once Ctrl+O expands it.
|
|
531
557
|
*/
|
|
532
|
-
#filterMatches(): number[] {
|
|
533
|
-
const
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
558
|
+
#filterMatches(): readonly number[] {
|
|
559
|
+
const query = this.#filter ?? "";
|
|
560
|
+
const native = isNativeRendering();
|
|
561
|
+
const memo = this.#filterMemo;
|
|
562
|
+
if (
|
|
563
|
+
memo &&
|
|
564
|
+
memo.query === query &&
|
|
565
|
+
memo.targets === this.#targets &&
|
|
566
|
+
memo.expanded === this.#expanded &&
|
|
567
|
+
memo.native === native &&
|
|
568
|
+
sameElements(memo.mainVisible, this.#mainVisible) &&
|
|
569
|
+
(native || sameElements(memo.mainRows, this.#mainRows))
|
|
570
|
+
) {
|
|
571
|
+
memo.mainRows = this.#mainRows;
|
|
572
|
+
memo.mainVisible = this.#mainVisible;
|
|
573
|
+
return memo.matches;
|
|
574
|
+
}
|
|
575
|
+
const patterns = this.#compileFilter(query);
|
|
576
|
+
if (native && this.#nativeTexts?.targets !== this.#targets) {
|
|
540
577
|
this.#nativeTexts = {
|
|
541
578
|
targets: this.#targets,
|
|
542
579
|
// Turn text plus its commands and tool output, like the expanded rendered rows.
|
|
@@ -551,21 +588,53 @@ export class RewindSelectorComponent implements Component {
|
|
|
551
588
|
for (let index = 0; index < this.#targets.length; index++) {
|
|
552
589
|
if (!this.#isMainSelectable(index)) continue;
|
|
553
590
|
const target = this.#targets[index]!;
|
|
554
|
-
const
|
|
555
|
-
?
|
|
556
|
-
:
|
|
557
|
-
let
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
this.#rowText.set(rows, text);
|
|
591
|
+
const matched = native
|
|
592
|
+
? patterns.every(pattern => pattern.test(this.#nativeTexts?.texts[index] ?? ""))
|
|
593
|
+
: patterns.every(pattern => {
|
|
594
|
+
for (let child = target.start; child < target.end; child++) {
|
|
595
|
+
const rows = this.#mainRows[child];
|
|
596
|
+
if (rows !== undefined && pattern.test(this.#plainRowText(rows))) return true;
|
|
561
597
|
}
|
|
562
|
-
return
|
|
598
|
+
return false;
|
|
563
599
|
});
|
|
564
|
-
if (
|
|
600
|
+
if (matched) matches.push(index);
|
|
565
601
|
}
|
|
602
|
+
this.#filterMemo = {
|
|
603
|
+
query,
|
|
604
|
+
targets: this.#targets,
|
|
605
|
+
expanded: this.#expanded,
|
|
606
|
+
native,
|
|
607
|
+
mainRows: this.#mainRows,
|
|
608
|
+
mainVisible: this.#mainVisible,
|
|
609
|
+
matches,
|
|
610
|
+
};
|
|
566
611
|
return matches;
|
|
567
612
|
}
|
|
568
613
|
|
|
614
|
+
/** Word patterns of `query`, compiled once per distinct query. */
|
|
615
|
+
#compileFilter(query: string): RegExp[] {
|
|
616
|
+
const cached = this.#filterPatterns;
|
|
617
|
+
if (cached?.query === query) return cached.patterns;
|
|
618
|
+
const words = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
619
|
+
const patterns = words.map(word =>
|
|
620
|
+
/^[\p{Script=Latin}\p{N}_]+$/u.test(word)
|
|
621
|
+
? new RegExp(`(?<![\\p{L}\\p{N}_])${RegExp.escape(word)}(?![\\p{L}\\p{N}_])`, "u")
|
|
622
|
+
: new RegExp(RegExp.escape(word), "u"),
|
|
623
|
+
);
|
|
624
|
+
this.#filterPatterns = { query, patterns };
|
|
625
|
+
return patterns;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/** Lowercased plain text of one rendered child row array, cached by array identity. */
|
|
629
|
+
#plainRowText(rows: readonly string[]): string {
|
|
630
|
+
let text = this.#rowText.get(rows);
|
|
631
|
+
if (text === undefined) {
|
|
632
|
+
text = Bun.stripANSI(rows.join("\n")).toLowerCase();
|
|
633
|
+
this.#rowText.set(rows, text);
|
|
634
|
+
}
|
|
635
|
+
return text;
|
|
636
|
+
}
|
|
637
|
+
|
|
569
638
|
/** Up/Down: step within the active column; leaving a sibling column's top exits the strip. */
|
|
570
639
|
#moveVertical(delta: -1 | 1): void {
|
|
571
640
|
if (this.#activeVariant > 0) {
|
|
@@ -58,6 +58,8 @@ export interface ObservableSession {
|
|
|
58
58
|
lastUpdate: number;
|
|
59
59
|
/** Latest progress snapshot from the subagent executor */
|
|
60
60
|
progress?: AgentProgress;
|
|
61
|
+
/** `Date.now()` when {@link progress} arrived; its `durationMs` was current then. */
|
|
62
|
+
progressAt?: number;
|
|
61
63
|
}
|
|
62
64
|
/** Coarse source of an observer change; callers use it to separate lifecycle work from high-frequency progress. */
|
|
63
65
|
export type SessionObserverChangeKind = "main" | "reset" | "lifecycle" | "progress";
|
|
@@ -64,6 +64,8 @@ export interface ObservableSession {
|
|
|
64
64
|
lastUpdate: number;
|
|
65
65
|
/** Latest progress snapshot from the subagent executor */
|
|
66
66
|
progress?: AgentProgress;
|
|
67
|
+
/** `Date.now()` when {@link progress} arrived; its `durationMs` was current then. */
|
|
68
|
+
progressAt?: number;
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
/** Coarse source of an observer change; callers use it to separate lifecycle work from high-frequency progress. */
|
|
@@ -225,6 +227,9 @@ export class SessionObserverRegistry {
|
|
|
225
227
|
existing.detached = payload.detached ?? existing.detached;
|
|
226
228
|
if (payload.description) existing.description = payload.description;
|
|
227
229
|
if (payload.sessionFile) existing.sessionFile = payload.sessionFile;
|
|
230
|
+
// A kept-alive or parked agent's next turn restarts its clock; the prior turn's
|
|
231
|
+
// snapshot must not be aged across the idle gap before the new turn's first frame.
|
|
232
|
+
if (payload.status === "started") existing.progressAt = undefined;
|
|
228
233
|
} else {
|
|
229
234
|
this.#sessions.set(payload.id, {
|
|
230
235
|
id: payload.id,
|
|
@@ -256,12 +261,14 @@ export class SessionObserverRegistry {
|
|
|
256
261
|
|
|
257
262
|
const sortOrder = this.#ensureSortOrder(id);
|
|
258
263
|
this.#ensureParentSortOrder(payload.parentToolCallId, sortOrder);
|
|
264
|
+
const now = Date.now();
|
|
259
265
|
if (existing) {
|
|
260
|
-
existing.lastUpdate =
|
|
266
|
+
existing.lastUpdate = now;
|
|
261
267
|
existing.index = payload.index;
|
|
262
268
|
existing.parentToolCallId = payload.parentToolCallId ?? existing.parentToolCallId;
|
|
263
269
|
existing.detached = payload.detached ?? existing.detached;
|
|
264
270
|
existing.progress = progress;
|
|
271
|
+
existing.progressAt = now;
|
|
265
272
|
if (progress.description) existing.description = progress.description;
|
|
266
273
|
if (payload.sessionFile) existing.sessionFile = payload.sessionFile;
|
|
267
274
|
} else {
|
|
@@ -276,8 +283,9 @@ export class SessionObserverRegistry {
|
|
|
276
283
|
parentToolCallId: payload.parentToolCallId,
|
|
277
284
|
detached: payload.detached,
|
|
278
285
|
index: payload.index,
|
|
279
|
-
lastUpdate:
|
|
286
|
+
lastUpdate: now,
|
|
280
287
|
progress,
|
|
288
|
+
progressAt: now,
|
|
281
289
|
});
|
|
282
290
|
}
|
|
283
291
|
this.#notifyListeners("progress");
|