@linxiraos/pi-tui 1.1.27 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -461
- package/README.md +7 -5
- package/dist/types/app-keybindings.d.ts +1 -1
- package/dist/types/apps/cleanse-board.d.ts +1 -1
- package/dist/types/apps/debug/log-viewer.d.ts +4 -0
- package/dist/types/apps/debug/raw-sse-buffer.d.ts +2 -0
- package/dist/types/apps/git/git-tui.d.ts +2 -2
- package/dist/types/apps/git/help.d.ts +3 -1
- package/dist/types/apps/if-bench-board.d.ts +2 -2
- package/dist/types/apps/setup-model-picker.d.ts +1 -1
- package/dist/types/autocomplete.d.ts +5 -2
- package/dist/types/charts/chart-plan.d.ts +674 -0
- package/dist/types/charts/chart-svg.d.ts +59 -0
- package/dist/types/charts/table-data.d.ts +175 -0
- package/dist/types/chat/assistant-message.d.ts +5 -0
- package/dist/types/chat/chat-transcript-builder.d.ts +2 -0
- package/dist/types/chat/compaction-summary-message.d.ts +1 -0
- package/dist/types/chat/execution-shared.d.ts +6 -5
- package/dist/types/chat/fence-figure.d.ts +42 -0
- package/dist/types/chat/figure-markdown.d.ts +42 -0
- package/dist/types/chat/messages.d.ts +1 -1
- package/dist/types/chat/svg-figure.d.ts +33 -0
- package/dist/types/chat/svg-source.d.ts +49 -0
- package/dist/types/chat/table-chart.d.ts +89 -0
- package/dist/types/chat/tool-execution.d.ts +2 -0
- package/dist/types/chat/user-message.d.ts +15 -2
- package/dist/types/chrome/shared.d.ts +9 -0
- package/dist/types/chrome/transcript-container.d.ts +6 -0
- package/dist/types/components/box.d.ts +1 -0
- package/dist/types/components/disclosure.d.ts +1 -0
- package/dist/types/components/editor.d.ts +15 -4
- package/dist/types/components/form.d.ts +1 -0
- package/dist/types/components/image.d.ts +12 -44
- package/dist/types/components/key-value-list.d.ts +1 -0
- package/dist/types/components/layout/row.d.ts +1 -0
- package/dist/types/components/layout/stack.d.ts +1 -0
- package/dist/types/components/markdown.d.ts +36 -3
- package/dist/types/components/scroll-view.d.ts +1 -0
- package/dist/types/components/section.d.ts +1 -0
- package/dist/types/components/table.d.ts +1 -0
- package/dist/types/components/text.d.ts +1 -0
- package/dist/types/components/tree-view.d.ts +7 -0
- package/dist/types/components/truncated-text.d.ts +1 -0
- package/dist/types/fuzzy.d.ts +29 -1
- package/dist/types/key-hint-format.d.ts +1 -1
- package/dist/types/keybinding-matchers.d.ts +3 -0
- package/dist/types/keybindings.d.ts +1 -1
- package/dist/types/kitty-graphics.d.ts +2 -2
- package/dist/types/loop-watchdog.d.ts +8 -7
- package/dist/types/multiplexers/cmux.d.ts +16 -0
- package/dist/types/multiplexers/herdr.d.ts +26 -0
- package/dist/types/multiplexers/notify.d.ts +13 -0
- package/dist/types/multiplexers/orca.d.ts +12 -0
- package/dist/types/multiplexers/screen.d.ts +9 -0
- package/dist/types/multiplexers/tmux.d.ts +29 -0
- package/dist/types/multiplexers/types.d.ts +100 -0
- package/dist/types/multiplexers/wmux.d.ts +12 -0
- package/dist/types/multiplexers/zellij.d.ts +12 -0
- package/dist/types/native/backend.d.ts +3 -2
- package/dist/types/native/blobs.d.ts +7 -3
- package/dist/types/native/encode.d.ts +3 -4
- package/dist/types/native/overlay.d.ts +5 -0
- package/dist/types/overlays/agent-hub-projection.d.ts +6 -5
- package/dist/types/overlays/agent-hub-types.d.ts +9 -0
- package/dist/types/overlays/agent-hub.d.ts +2 -0
- package/dist/types/overlays/agent-transcript-viewer.d.ts +11 -2
- package/dist/types/overlays/agents-hub.d.ts +9 -0
- package/dist/types/overlays/annotation-overlay.d.ts +19 -2
- package/dist/types/overlays/annotation-types.d.ts +2 -0
- package/dist/types/overlays/ask-dialog.d.ts +5 -2
- package/dist/types/overlays/history-search.d.ts +10 -0
- package/dist/types/overlays/hook-selector.d.ts +9 -4
- package/dist/types/overlays/jobs-panel.d.ts +1 -7
- package/dist/types/overlays/model-browser.d.ts +54 -7
- package/dist/types/overlays/model-hub.d.ts +23 -0
- package/dist/types/overlays/oauth-selector.d.ts +0 -1
- package/dist/types/overlays/plan-review-overlay.d.ts +3 -2
- package/dist/types/overlays/session-observer-registry.d.ts +2 -0
- package/dist/types/overlays/session-selector.d.ts +6 -2
- package/dist/types/overlays/settings-defs.d.ts +1 -0
- package/dist/types/overlays/usage-dashboard.d.ts +8 -4
- package/dist/types/overlays/usage-display.d.ts +3 -1
- package/dist/types/prompt/composer.d.ts +2 -7
- package/dist/types/prompt/custom-editor.d.ts +11 -8
- package/dist/types/prompt/image-source.d.ts +2 -1
- package/dist/types/prompt/interactive-context-helpers.d.ts +4 -1
- package/dist/types/prompt/model-mention-autocomplete.d.ts +1 -1
- package/dist/types/prompt/welcome.d.ts +19 -42
- package/dist/types/render/composite.d.ts +2 -0
- package/dist/types/render/output-pane.d.ts +1 -0
- package/dist/types/render/render-utils.d.ts +1 -0
- package/dist/types/render/tool-card.d.ts +6 -0
- package/dist/types/render/width-aware-text.d.ts +2 -0
- package/dist/types/setup/scenes/splash.d.ts +1 -1
- package/dist/types/setup/scenes/types.d.ts +1 -0
- package/dist/types/status-line/component.d.ts +3 -0
- package/dist/types/status-line/host.d.ts +4 -1
- package/dist/types/status-line/segments.d.ts +2 -0
- package/dist/types/status-line/types.d.ts +14 -3
- package/dist/types/terminal-capabilities.d.ts +27 -35
- package/dist/types/terminal-multiplexer.d.ts +127 -14
- package/dist/types/terminal.d.ts +22 -6
- package/dist/types/theme/active-symbols.d.ts +1 -1
- package/dist/types/theme/schema.d.ts +11 -2
- package/dist/types/theme/symbols.d.ts +1 -1
- package/dist/types/theme/theme-class.d.ts +7 -0
- package/dist/types/theme/theme.d.ts +1 -1
- package/dist/types/theme/tui-adapters.d.ts +3 -0
- package/dist/types/tmux.d.ts +11 -4
- package/dist/types/tools/daemon.d.ts +1 -1
- package/dist/types/tools/eval.d.ts +22 -3
- package/dist/types/tools/glob.d.ts +4 -0
- package/dist/types/tools/native-view.d.ts +47 -0
- package/dist/types/tools/output-meta.d.ts +11 -1
- package/dist/types/tools/renderer.d.ts +37 -2
- package/dist/types/tools/streaming-output.d.ts +20 -1
- package/dist/types/tools/task.d.ts +0 -4
- package/dist/types/tools/write.d.ts +9 -2
- package/dist/types/tools/yield.d.ts +22 -0
- package/dist/types/tui.d.ts +31 -0
- package/dist/types/utils.d.ts +6 -0
- package/dist/types/vim.d.ts +19 -5
- package/package.json +13 -9
- package/src/app-keybindings.d.ts +1 -1
- package/src/app-keybindings.ts +1 -1
- package/src/apps/autoresearch-dashboard.ts +7 -3
- package/src/apps/cleanse-board.ts +2 -2
- package/src/apps/cleanse-picker.ts +1 -1
- package/src/apps/debug/log-viewer.ts +188 -72
- package/src/apps/debug/raw-sse-buffer.ts +8 -0
- package/src/apps/debug/raw-sse.ts +39 -5
- package/src/apps/git/diff-pane.ts +26 -5
- package/src/apps/git/git-tui.ts +2 -2
- package/src/apps/git/help.ts +29 -20
- package/src/apps/if-bench-board.ts +2 -2
- package/src/apps/ps-top.ts +2 -2
- package/src/apps/setup-model-picker.ts +1 -1
- package/src/autocomplete.d.ts +5 -2
- package/src/autocomplete.ts +67 -43
- package/src/charts/chart-plan.d.ts +674 -0
- package/src/charts/chart-plan.ts +5081 -0
- package/src/charts/chart-svg.d.ts +59 -0
- package/src/charts/chart-svg.ts +4657 -0
- package/src/charts/table-data.d.ts +175 -0
- package/src/charts/table-data.ts +1478 -0
- package/src/chat/advisor-message.ts +11 -0
- package/src/chat/assistant-message.d.ts +5 -0
- package/src/chat/assistant-message.ts +219 -50
- package/src/chat/chat-transcript-builder.d.ts +2 -0
- package/src/chat/chat-transcript-builder.ts +5 -3
- package/src/chat/compaction-summary-message.d.ts +1 -0
- package/src/chat/compaction-summary-message.ts +8 -0
- package/src/chat/execution-shared.d.ts +6 -5
- package/src/chat/execution-shared.ts +42 -25
- package/src/chat/fence-figure.d.ts +42 -0
- package/src/chat/fence-figure.ts +80 -0
- package/src/chat/figure-markdown.d.ts +42 -0
- package/src/chat/figure-markdown.ts +174 -0
- package/src/chat/messages.d.ts +1 -1
- package/src/chat/messages.ts +1 -1
- package/src/chat/read-tool-group.ts +6 -4
- package/src/chat/skill-message.ts +6 -0
- package/src/chat/svg-figure.d.ts +33 -0
- package/src/chat/svg-figure.ts +399 -0
- package/src/chat/svg-source.d.ts +49 -0
- package/src/chat/svg-source.ts +188 -0
- package/src/chat/table-chart.d.ts +89 -0
- package/src/chat/table-chart.ts +294 -0
- package/src/chat/tool-execution.d.ts +2 -0
- package/src/chat/tool-execution.ts +80 -6
- package/src/chat/user-message.d.ts +15 -2
- package/src/chat/user-message.ts +55 -3
- package/src/chrome/shared.ts +53 -0
- package/src/chrome/transcript-container.d.ts +6 -0
- package/src/chrome/transcript-container.ts +93 -13
- package/src/components/box.d.ts +1 -0
- package/src/components/box.ts +5 -0
- package/src/components/disclosure.d.ts +1 -0
- package/src/components/disclosure.ts +8 -0
- package/src/components/editor.d.ts +15 -4
- package/src/components/editor.ts +294 -46
- package/src/components/form.d.ts +1 -0
- package/src/components/form.ts +8 -0
- package/src/components/image.d.ts +12 -44
- package/src/components/image.ts +47 -151
- package/src/components/key-value-list.d.ts +1 -0
- package/src/components/key-value-list.ts +4 -0
- package/src/components/layout/row.d.ts +1 -0
- package/src/components/layout/row.ts +7 -0
- package/src/components/layout/stack.d.ts +1 -0
- package/src/components/layout/stack.ts +7 -0
- package/src/components/markdown.d.ts +36 -3
- package/src/components/markdown.ts +433 -318
- package/src/components/scroll-view.d.ts +1 -0
- package/src/components/scroll-view.ts +5 -0
- package/src/components/section.d.ts +1 -0
- package/src/components/section.ts +5 -0
- package/src/components/settings-list.ts +2 -0
- package/src/components/table.d.ts +1 -0
- package/src/components/table.ts +4 -0
- package/src/components/text.d.ts +1 -0
- package/src/components/text.ts +7 -0
- package/src/components/tree-view.d.ts +7 -0
- package/src/components/tree-view.ts +38 -8
- package/src/components/truncated-text.d.ts +1 -0
- package/src/components/truncated-text.ts +5 -0
- package/src/desktop-notify.ts +1 -1
- package/src/fuzzy.d.ts +29 -1
- package/src/fuzzy.ts +115 -30
- package/src/key-hint-format.d.ts +1 -1
- package/src/key-hint-format.ts +1 -1
- package/src/keybinding-matchers.d.ts +3 -0
- package/src/keybinding-matchers.ts +6 -2
- package/src/keybindings.d.ts +1 -1
- package/src/keybindings.ts +1 -1
- package/src/kitty-graphics.d.ts +2 -2
- package/src/kitty-graphics.ts +4 -4
- package/src/loop-watchdog.d.ts +8 -7
- package/src/loop-watchdog.ts +18 -13
- package/src/multiplexers/cmux.d.ts +16 -0
- package/src/multiplexers/cmux.ts +29 -0
- package/src/multiplexers/herdr.d.ts +26 -0
- package/src/multiplexers/herdr.ts +64 -0
- package/src/multiplexers/notify.d.ts +13 -0
- package/src/multiplexers/notify.ts +24 -0
- package/src/multiplexers/orca.d.ts +12 -0
- package/src/multiplexers/orca.ts +16 -0
- package/src/multiplexers/screen.d.ts +9 -0
- package/src/multiplexers/screen.ts +15 -0
- package/src/multiplexers/tmux.d.ts +29 -0
- package/src/multiplexers/tmux.ts +40 -0
- package/src/multiplexers/types.d.ts +100 -0
- package/src/multiplexers/types.ts +110 -0
- package/src/multiplexers/wmux.d.ts +12 -0
- package/src/multiplexers/wmux.ts +16 -0
- package/src/multiplexers/zellij.d.ts +12 -0
- package/src/multiplexers/zellij.ts +24 -0
- package/src/native/backend.d.ts +3 -2
- package/src/native/backend.ts +174 -18
- package/src/native/blobs.d.ts +7 -3
- package/src/native/blobs.ts +47 -12
- package/src/native/encode.d.ts +3 -4
- package/src/native/encode.ts +58 -31
- package/src/native/overlay.d.ts +5 -0
- package/src/native/overlay.ts +13 -0
- package/src/native/reconcile.ts +4 -0
- package/src/overlays/agent-hub-projection.d.ts +6 -5
- package/src/overlays/agent-hub-projection.ts +124 -13
- package/src/overlays/agent-hub-types.d.ts +9 -0
- package/src/overlays/agent-hub-types.ts +6 -0
- package/src/overlays/agent-hub.ts +46 -31
- package/src/overlays/agent-transcript-viewer.ts +91 -23
- package/src/overlays/agents-hub.ts +28 -8
- package/src/overlays/annotation-overlay.ts +293 -90
- package/src/overlays/annotation-types.ts +2 -0
- package/src/overlays/ask-dialog.ts +7 -10
- package/src/overlays/btw-history-panel.ts +12 -1
- package/src/overlays/copy-selector.ts +30 -8
- package/src/overlays/extensions/state-manager.ts +52 -12
- package/src/overlays/history-search.ts +100 -7
- package/src/overlays/hook-selector.ts +12 -5
- package/src/overlays/jobs-panel.ts +28 -46
- package/src/overlays/model-browser.ts +341 -110
- package/src/overlays/model-hub.ts +300 -49
- package/src/overlays/move-overlay.ts +28 -14
- package/src/overlays/oauth-selector.ts +54 -70
- package/src/overlays/plan-review-overlay.ts +194 -70
- package/src/overlays/plugin-settings.ts +11 -5
- package/src/overlays/report-panel.ts +2 -2
- package/src/overlays/rewind-selector.ts +86 -17
- package/src/overlays/session-observer-registry.d.ts +2 -0
- package/src/overlays/session-observer-registry.ts +10 -2
- package/src/overlays/session-selector.ts +64 -26
- package/src/overlays/settings-defs.ts +4 -3
- package/src/overlays/settings-selector.ts +33 -8
- package/src/overlays/snapcompact-shape-preview.ts +2 -3
- package/src/overlays/stats-notice.ts +1 -1
- package/src/overlays/tree-selector.ts +41 -20
- package/src/overlays/usage-dashboard.ts +100 -50
- package/src/overlays/usage-display.ts +10 -4
- package/src/prompt/attachment-chips.ts +15 -11
- package/src/prompt/composer-attachments.ts +6 -4
- package/src/prompt/composer-cache.ts +4 -6
- package/src/prompt/composer.ts +6 -37
- package/src/prompt/custom-editor.ts +54 -26
- package/src/prompt/image-source.ts +2 -1
- package/src/prompt/interactive-context-helpers.ts +30 -18
- package/src/prompt/macos-spelling.ts +7 -0
- package/src/prompt/magic-keywords.ts +11 -2
- package/src/prompt/model-mention-autocomplete.ts +14 -14
- package/src/prompt/tips.txt +6 -5
- package/src/prompt/welcome.ts +235 -411
- package/src/prompt/word-completion.ts +38 -5
- package/src/render/composite.d.ts +2 -0
- package/src/render/composite.ts +64 -0
- package/src/render/output-pane.d.ts +1 -0
- package/src/render/output-pane.ts +6 -0
- package/src/render/render-utils.d.ts +1 -0
- package/src/render/render-utils.ts +11 -0
- package/src/render/tool-card.d.ts +6 -0
- package/src/render/tool-card.ts +15 -0
- package/src/render/width-aware-text.d.ts +2 -0
- package/src/render/width-aware-text.ts +5 -0
- package/src/setup/scenes/model.ts +2 -1
- package/src/setup/scenes/splash.ts +116 -53
- package/src/setup/scenes/types.ts +1 -0
- package/src/status-line/component.ts +292 -78
- package/src/status-line/host.ts +4 -1
- package/src/status-line/segments.ts +40 -22
- package/src/status-line/types.ts +15 -3
- package/src/terminal-capabilities.d.ts +27 -35
- package/src/terminal-capabilities.ts +75 -189
- package/src/terminal-multiplexer.d.ts +127 -14
- package/src/terminal-multiplexer.ts +112 -33
- package/src/terminal.d.ts +22 -6
- package/src/terminal.ts +139 -37
- package/src/theme/active-symbols.d.ts +1 -1
- package/src/theme/active-symbols.ts +1 -1
- package/src/theme/schema-validation.ts +1 -0
- package/src/theme/schema.d.ts +11 -2
- package/src/theme/schema.ts +14 -1
- package/src/theme/session-color.ts +1 -7
- package/src/theme/symbols.d.ts +1 -1
- package/src/theme/symbols.ts +6 -1
- package/src/theme/theme-class.d.ts +7 -0
- package/src/theme/theme-class.ts +26 -1
- package/src/theme/theme-schema.json +5 -1
- package/src/theme/theme.d.ts +1 -1
- package/src/theme/theme.ts +8 -4
- package/src/theme/tui-adapters.d.ts +3 -0
- package/src/theme/tui-adapters.ts +68 -0
- package/src/tmux.d.ts +11 -4
- package/src/tmux.ts +47 -11
- package/src/tools/ast-edit.ts +59 -35
- package/src/tools/ast-grep.ts +35 -16
- package/src/tools/bash.ts +92 -42
- package/src/tools/daemon.d.ts +1 -1
- package/src/tools/daemon.ts +1 -1
- package/src/tools/default-renderer.ts +35 -19
- package/src/tools/edit.ts +142 -120
- package/src/tools/eval-format/index.ts +28 -2
- package/src/tools/eval-format/javascript.ts +7 -5
- package/src/tools/eval.d.ts +22 -3
- package/src/tools/eval.ts +364 -208
- package/src/tools/fetch.ts +5 -6
- package/src/tools/glob.d.ts +4 -0
- package/src/tools/glob.ts +33 -65
- package/src/tools/grep.ts +81 -48
- package/src/tools/index.ts +2 -0
- package/src/tools/lsp.ts +81 -59
- package/src/tools/mcp.ts +111 -68
- package/src/tools/native-view.d.ts +47 -0
- package/src/tools/native-view.ts +102 -1
- package/src/tools/output-meta.d.ts +11 -1
- package/src/tools/output-meta.ts +23 -3
- package/src/tools/read.ts +7 -5
- package/src/tools/renderer.d.ts +37 -2
- package/src/tools/renderer.ts +42 -2
- package/src/tools/streaming-output.d.ts +20 -1
- package/src/tools/streaming-output.ts +149 -50
- package/src/tools/task.d.ts +0 -4
- package/src/tools/task.ts +112 -59
- package/src/tools/todo.ts +119 -30
- package/src/tools/wait.ts +34 -24
- package/src/tools/web-search.ts +1 -1
- package/src/tools/write.d.ts +9 -2
- package/src/tools/write.ts +147 -95
- package/src/tools/yield.d.ts +22 -0
- package/src/tools/yield.ts +225 -0
- package/src/tui.d.ts +31 -0
- package/src/tui.ts +309 -160
- package/src/utils.d.ts +6 -0
- package/src/utils.ts +19 -0
- package/src/vim.d.ts +19 -5
- package/src/vim.ts +333 -18
- package/src/windows-input-mode.ts +8 -0
package/src/prompt/welcome.ts
CHANGED
|
@@ -2,45 +2,73 @@ import { APP_NAME } from "@linxiraos/pi-utils/dirs";
|
|
|
2
2
|
import type { TspSpan } from "@linxiraos/pi-wire";
|
|
3
3
|
import { formatDoubleTap, formatKeyHint, formatKeyHints, type KeyName } from "../app-keybindings";
|
|
4
4
|
import { editorKey } from "../chrome/keybinding-hints";
|
|
5
|
+
import { tuiText } from "../i18n";
|
|
5
6
|
import { getKeybindings, type Keybinding } from "../keybindings";
|
|
6
|
-
import { card, col,
|
|
7
|
-
import type { DescribeContext, NativeChild, NativeNode
|
|
8
|
-
import {
|
|
9
|
-
import { plainLine } from "../native/spans";
|
|
7
|
+
import { card, col, keyed, node, row, span, text } from "../native/describe";
|
|
8
|
+
import type { DescribeContext, NativeChild, NativeNode } from "../native/node";
|
|
9
|
+
import { compactText, plainText } from "../native/spans";
|
|
10
10
|
import { isNativeRendering } from "../native/state";
|
|
11
|
+
import { isHyperlinkEnabled, urlHyperlink, urlLinkSpan } from "../render/hyperlink";
|
|
11
12
|
import { TERMINAL } from "../terminal-capabilities";
|
|
12
13
|
import { theme } from "../theme/theme";
|
|
13
14
|
import type { Component } from "../tui";
|
|
14
|
-
import { padding, replaceTabs,
|
|
15
|
+
import { padding, replaceTabs, visibleWidth, wrapTextWithAnsi } from "../utils";
|
|
15
16
|
import tipsText from "./tips.txt" with { type: "text" };
|
|
16
17
|
|
|
17
|
-
/**
|
|
18
|
-
*
|
|
19
|
-
const
|
|
20
|
-
.split("\n")
|
|
21
|
-
.map(line => line.trim())
|
|
22
|
-
.filter(line => line.length > 0);
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Fixed number of session rows in the welcome box so its height stays stable
|
|
26
|
-
* across recent-session updates.
|
|
27
|
-
*/
|
|
28
|
-
export const WELCOME_SESSION_SLOTS = 4;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Fixed number of LSP-server rows, for the same reason. Overflow is sliced so
|
|
32
|
-
* the box height is constant regardless of how many servers a project has.
|
|
33
|
-
*/
|
|
34
|
-
export const WELCOME_LSP_SLOTS = 4;
|
|
18
|
+
/** Leading marker for a tip that pitches Tern: shown only while no TSP surface
|
|
19
|
+
* is live, since a live one means the user is already in Tern. */
|
|
20
|
+
const NO_TERN_TIP_MARKER = /^\[NO-TERN\]\s*/;
|
|
35
21
|
|
|
36
22
|
/** Trailing marker that flags a tip as a "what's new" callout. Stripped before
|
|
37
23
|
* wrapping (with any preceding whitespace) and replaced by {@link NEW_TAG_TEXT}
|
|
38
24
|
* painted as a shimmering rainbow. Non-global so `.test` stays stateless. */
|
|
39
25
|
const NEW_TIP_MARKER = /\s*\[NEW\]\s*$/;
|
|
40
26
|
|
|
27
|
+
/** A tips.txt line. The structural markers stay in the source line — they drive
|
|
28
|
+
* pool selection and the "[NEW]" weighting — while the user-visible text is
|
|
29
|
+
* resolved from the host catalogue under `tipN`, N = the 1-based line number. */
|
|
30
|
+
interface TipLine {
|
|
31
|
+
/** 1-based tips.txt line number; the catalogue key is `tip${line}`. */
|
|
32
|
+
readonly line: number;
|
|
33
|
+
/** Raw line, markers intact — what the weighted picker selects from. */
|
|
34
|
+
readonly raw: string;
|
|
35
|
+
/** English body with both markers stripped: the `tuiText` fallback. */
|
|
36
|
+
readonly english: string;
|
|
37
|
+
/** Pitches Tern; excluded from the TSP pool. */
|
|
38
|
+
readonly noTern: boolean;
|
|
39
|
+
/** "What's new" callout; carries the heavier selection weight. */
|
|
40
|
+
readonly isNew: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** tips.txt lines embedded at build time; blanks dropped. Key placeholders
|
|
44
|
+
* (see {@link expandTipKeys}) and markdown links stay raw until render time. */
|
|
45
|
+
const TIP_LINES: readonly TipLine[] = tipsText.split("\n").flatMap((line, index) => {
|
|
46
|
+
const raw = line.trim();
|
|
47
|
+
if (raw.length === 0) return [];
|
|
48
|
+
return [
|
|
49
|
+
{
|
|
50
|
+
line: index + 1,
|
|
51
|
+
raw,
|
|
52
|
+
english: raw.replace(NO_TERN_TIP_MARKER, "").replace(NEW_TIP_MARKER, ""),
|
|
53
|
+
noTern: NO_TERN_TIP_MARKER.test(raw),
|
|
54
|
+
isNew: NEW_TIP_MARKER.test(raw),
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/** Classic pool: every tip's raw line, so {@link pickWeightedTip} can still read
|
|
60
|
+
* the trailing {@link NEW_TIP_MARKER} for its selection weight. */
|
|
61
|
+
const TIPS: readonly string[] = TIP_LINES.map(tip => tip.raw);
|
|
62
|
+
|
|
63
|
+
/** TSP pool: those marked {@link NO_TERN_TIP_MARKER} dropped. */
|
|
64
|
+
const TSP_TIPS: readonly string[] = TIP_LINES.filter(tip => !tip.noTern).map(tip => tip.raw);
|
|
65
|
+
|
|
41
66
|
/** Visible text rendered in place of {@link NEW_TIP_MARKER}. */
|
|
42
67
|
const NEW_TAG_TEXT = "NEW!";
|
|
43
68
|
|
|
69
|
+
/** Inline English for the nerdfont nudge tip (`welcomeNerdFontJoke`). */
|
|
70
|
+
const NERDFONT_TIP = "Please use nerdfont 😭.";
|
|
71
|
+
|
|
44
72
|
/** Milliseconds for one full hue rotation of the rainbow "NEW!" tag. */
|
|
45
73
|
const NEW_GLOW_PERIOD_MS = 1500;
|
|
46
74
|
|
|
@@ -63,16 +91,33 @@ export function pickWeightedTip(tips: readonly string[], r: number): string {
|
|
|
63
91
|
return tips[tips.length - 1] ?? "";
|
|
64
92
|
}
|
|
65
93
|
|
|
94
|
+
/** A tip resolved through the host catalogue: the localised body plus the
|
|
95
|
+
* structural "[NEW]" flag, which lives in tips.txt rather than the translated
|
|
96
|
+
* text (so a translation never has to carry the marker). */
|
|
97
|
+
interface ResolvedTip {
|
|
98
|
+
readonly text: string;
|
|
99
|
+
readonly isNew: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Localise a picked tip. `raw` is a {@link TIPS}/{@link TSP_TIPS} member (or the
|
|
103
|
+
* nerdfont nudge); the catalogue is consulted live, so `/language` applies. */
|
|
104
|
+
function resolveTip(raw: string): ResolvedTip {
|
|
105
|
+
if (raw === NERDFONT_TIP) return { text: tuiText("welcomeNerdFontJoke", NERDFONT_TIP), isNew: false };
|
|
106
|
+
const entry = TIP_LINES.find(tip => tip.raw === raw);
|
|
107
|
+
if (entry === undefined) return { text: raw, isNew: NEW_TIP_MARKER.test(raw) };
|
|
108
|
+
return { text: tuiText(`tip${entry.line}`, entry.english), isNew: entry.isNew };
|
|
109
|
+
}
|
|
110
|
+
|
|
66
111
|
type ColorEncoding = "ansi-16m" | "ansi-256";
|
|
67
112
|
|
|
68
|
-
/** Paint each glyph of
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
function renderNewTag(phase: number, encoding: ColorEncoding): string {
|
|
113
|
+
/** Paint each glyph of `tagText` on a moving HSL rainbow. `phase` rotates the
|
|
114
|
+
* hue offset cyclically; successive renders with increasing phase shimmer,
|
|
115
|
+
* while a fixed phase yields a still rainbow. */
|
|
116
|
+
function renderNewTag(tagText: string, phase: number, encoding: ColorEncoding): string {
|
|
72
117
|
const bold = "\x1b[1m";
|
|
73
118
|
const reset = "\x1b[0m";
|
|
74
119
|
const wrapped = ((phase % 1) + 1) % 1;
|
|
75
|
-
const chars = [...
|
|
120
|
+
const chars = [...tagText];
|
|
76
121
|
let out = bold;
|
|
77
122
|
let prev = "";
|
|
78
123
|
for (let i = 0; i < chars.length; i++) {
|
|
@@ -120,96 +165,114 @@ function expandTipKeys(tip: string): string {
|
|
|
120
165
|
});
|
|
121
166
|
}
|
|
122
167
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
168
|
+
/** Markdown-style links in tips.txt: `[Tern](https://scl.so/tern)` shows the label, linked to the URL. */
|
|
169
|
+
const TIP_LINK = /\[([^\]]+)\]\(([^\s)]+)\)/g;
|
|
170
|
+
|
|
171
|
+
/** Tip links as underlined OSC 8 labels, or `label (url)` where the terminal cannot link. */
|
|
172
|
+
function linkTipText(tip: string): string {
|
|
173
|
+
const linkable = isHyperlinkEnabled();
|
|
174
|
+
return tip.replace(TIP_LINK, (_link, label: string, url: string) =>
|
|
175
|
+
linkable ? urlHyperlink(url, theme.underline(label)) : `${label} (${url})`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The tip as native text: escapes dropped, links as `href` spans. */
|
|
180
|
+
function tipSpans(tip: string): TspSpan[] {
|
|
181
|
+
const spans: TspSpan[] = [];
|
|
182
|
+
let cursor = 0;
|
|
183
|
+
for (const match of tip.matchAll(TIP_LINK)) {
|
|
184
|
+
const [link, label, url] = match;
|
|
185
|
+
if (match.index > cursor) spans.push(span(plainText(tip.slice(cursor, match.index))));
|
|
186
|
+
spans.push(urlLinkSpan(url, label));
|
|
187
|
+
cursor = match.index + link.length;
|
|
188
|
+
}
|
|
189
|
+
if (cursor < tip.length) spans.push(span(plainText(tip.slice(cursor))));
|
|
190
|
+
return spans;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The welcome tip as lines of at most `width` columns: the `welcomeTipLabel`
|
|
195
|
+
* label and the body wrapped together, with no indent, so the banner can center
|
|
196
|
+
* each line. `isNew` drives the rainbow "welcomeNewTag" suffix; it is passed in
|
|
197
|
+
* because the marker lives in tips.txt, not in the resolved body. `[]` when
|
|
198
|
+
* `width` leaves no room for a useful line.
|
|
199
|
+
*/
|
|
200
|
+
export function renderWelcomeTip(tip: string, width: number, phase = 0, isNew = NEW_TIP_MARKER.test(tip)): string[] {
|
|
201
|
+
const label = tuiText("welcomeTipLabel", "Tip: ");
|
|
202
|
+
if (width - visibleWidth(label) < 8) return [];
|
|
128
203
|
|
|
129
|
-
const
|
|
130
|
-
const body = expandTipKeys(isNew ? tip.replace(NEW_TIP_MARKER, "") : tip);
|
|
204
|
+
const body = linkTipText(expandTipKeys(isNew ? tip.replace(NEW_TIP_MARKER, "") : tip));
|
|
131
205
|
|
|
132
|
-
|
|
133
|
-
|
|
206
|
+
// Trailing spaces left by the wrap would count toward the width the banner centers.
|
|
207
|
+
const wrapped = wrapTextWithAnsi(replaceTabs(`${label}${body}`), width).map(line => line.trimEnd());
|
|
208
|
+
if (wrapped.length === 0) return [];
|
|
134
209
|
|
|
135
210
|
// Pull both colors from the active theme so the line stays readable on light
|
|
136
211
|
// themes; the previous hardcoded `#b48cff` / `#9ccfff` pastels (plus a manual
|
|
137
212
|
// `\x1b[2m` dim on the body) dropped to ~1.5:1 contrast on a white background.
|
|
138
|
-
const continuationIndent = padding(labelWidth);
|
|
139
213
|
const styledLabel = theme.fg("customMessageLabel", label);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
214
|
+
const lines = wrapped.map((line, index) =>
|
|
215
|
+
theme.italic(
|
|
216
|
+
index === 0 && line.startsWith(label)
|
|
217
|
+
? `${styledLabel}${theme.fg("muted", line.slice(label.length))}`
|
|
218
|
+
: theme.fg("muted", line),
|
|
219
|
+
),
|
|
220
|
+
);
|
|
146
221
|
|
|
147
222
|
if (isNew) {
|
|
148
|
-
// Append the rainbow tag to the final
|
|
149
|
-
//
|
|
150
|
-
// styled glyphs never overflow or reflow the wrapped body.
|
|
223
|
+
// Append the rainbow tag to the final line when it fits; otherwise give it
|
|
224
|
+
// a line of its own so the styled glyphs never overflow the width.
|
|
151
225
|
const encoding: ColorEncoding = TERMINAL.trueColor ? "ansi-16m" : "ansi-256";
|
|
152
|
-
const
|
|
153
|
-
const
|
|
226
|
+
const tagText = tuiText("welcomeNewTag", NEW_TAG_TEXT);
|
|
227
|
+
const tag = renderNewTag(tagText, phase, encoding);
|
|
228
|
+
const tagWidth = 1 + visibleWidth(tagText); // 1 = space separator
|
|
154
229
|
const lastLine = lines[lines.length - 1];
|
|
155
|
-
if (lastLine !== undefined && visibleWidth(lastLine) + tagWidth <=
|
|
230
|
+
if (lastLine !== undefined && visibleWidth(lastLine) + tagWidth <= width) {
|
|
156
231
|
lines[lines.length - 1] = `${lastLine} ${tag}`;
|
|
157
232
|
} else {
|
|
158
|
-
lines.push(
|
|
233
|
+
lines.push(tag);
|
|
159
234
|
}
|
|
160
235
|
}
|
|
161
236
|
|
|
162
237
|
return lines;
|
|
163
238
|
}
|
|
164
239
|
|
|
165
|
-
export interface RecentSession {
|
|
166
|
-
name: string;
|
|
167
|
-
timeAgo: string;
|
|
168
|
-
/** Session file; a native click on the row resumes it. */
|
|
169
|
-
path?: string;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export interface LspServerInfo {
|
|
173
|
-
name: string;
|
|
174
|
-
status: "ready" | "error" | "connecting" | "available";
|
|
175
|
-
fileTypes: string[];
|
|
176
|
-
}
|
|
177
|
-
|
|
178
240
|
/**
|
|
179
|
-
*
|
|
241
|
+
* The session's welcome banner. In a terminal: the gradient logo beside the
|
|
242
|
+
* `zeta` wordmark with the version under it (the logo alone when the lockup does
|
|
243
|
+
* not fit) and the tip of the session (dropped below {@link TIP_MIN_COLUMNS}
|
|
244
|
+
* columns). Natively: a card with the same logo, wordmark, version and tip
|
|
245
|
+
* ({@link WelcomeComponent.describe}).
|
|
180
246
|
*/
|
|
181
247
|
export class WelcomeComponent implements Component {
|
|
182
248
|
#animStart: number | null = null;
|
|
183
249
|
#animTimer: Timer | null = null;
|
|
184
250
|
#requestRender: (() => void) | null = null;
|
|
185
251
|
// Tip randomness is latched once so the tip is stable across renders, but
|
|
186
|
-
// the nerdfont-nag gate re-reads the live preset
|
|
187
|
-
// run under the default "unicode"
|
|
188
|
-
//
|
|
252
|
+
// the nerdfont-nag gate re-reads the live preset and the pool re-reads the
|
|
253
|
+
// live TSP state: the startup prepaint can run under the default "unicode"
|
|
254
|
+
// preset before settings resolve the real one, and an optimistic Tern start
|
|
255
|
+
// falls back to the classic renderer when `hello` never confirms.
|
|
189
256
|
#nagRoll: number | undefined;
|
|
190
257
|
#tipRoll: number | undefined;
|
|
191
258
|
// Render cache: the welcome box is the first transcript-area component, so
|
|
192
259
|
// returning a stable array reference keeps the whole frame prefix stable.
|
|
193
|
-
// Bypassed while the intro animation runs (every frame differs).
|
|
260
|
+
// Bypassed while the intro animation runs (every frame differs). The
|
|
261
|
+
// `#cachedLang` sentinel retires the cache when `/language` swaps the
|
|
262
|
+
// catalogue, so a live switch repaints instead of serving stale text.
|
|
194
263
|
#cachedWidth = -1;
|
|
195
264
|
#cachedLines: string[] | undefined;
|
|
196
|
-
#
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
private modelName: string = "",
|
|
201
|
-
private providerName: string = "",
|
|
202
|
-
private recentSessions: RecentSession[] = [],
|
|
203
|
-
/** Detected project servers; `null` means LSP is disabled and hides the section. */
|
|
204
|
-
private lspServers: LspServerInfo[] | null = [],
|
|
205
|
-
) {}
|
|
265
|
+
#cachedLang = "";
|
|
266
|
+
#native: { lang: string; tip: string | undefined; node: NativeNode } | undefined;
|
|
267
|
+
|
|
268
|
+
constructor(private version: string) {}
|
|
206
269
|
get tip(): string | undefined {
|
|
207
270
|
this.#nagRoll ??= Math.random();
|
|
208
271
|
this.#tipRoll ??= Math.random();
|
|
209
272
|
if (theme.getSymbolPreset() === "unicode" && this.#nagRoll < 0.1) {
|
|
210
|
-
return
|
|
273
|
+
return NERDFONT_TIP;
|
|
211
274
|
}
|
|
212
|
-
return pickWeightedTip(TIPS, this.#tipRoll) || undefined;
|
|
275
|
+
return pickWeightedTip(isNativeRendering() ? TSP_TIPS : TIPS, this.#tipRoll) || undefined;
|
|
213
276
|
}
|
|
214
277
|
|
|
215
278
|
invalidate(): void {
|
|
@@ -219,25 +282,23 @@ export class WelcomeComponent implements Component {
|
|
|
219
282
|
}
|
|
220
283
|
|
|
221
284
|
/**
|
|
222
|
-
* A `card` (`zeta.welcome`)
|
|
223
|
-
* (`zeta.welcome.
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* carry the look (gradient logo, type scale, column hairline); a "[NEW]" tip
|
|
285
|
+
* A `card` (`zeta.welcome`) mirroring the terminal banner: the lockup
|
|
286
|
+
* (`zeta.welcome.lockup`: the terminal's builtin `zeta` mark, which it animates,
|
|
287
|
+
* beside the wordmark with the version under it) and the tip of the session.
|
|
288
|
+
* Roles carry the look (gradient logo, type scale); a "[NEW]" tip
|
|
227
289
|
* carries a terminal-clocked shimmering tag.
|
|
228
290
|
*/
|
|
229
291
|
describe(_cx: DescribeContext): NativeNode {
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
//
|
|
292
|
+
const lang = tuiText("welcomeBack", "Welcome back!");
|
|
293
|
+
const rawTip = this.tip;
|
|
294
|
+
const tip = rawTip === undefined ? undefined : resolveTip(rawTip);
|
|
295
|
+
if (this.#native && this.#native.lang === lang && this.#native.tip === tip?.text) return this.#native.node;
|
|
296
|
+
// Brand lines are short and fixed; never wrap or truncate them.
|
|
235
297
|
const art = (spans: readonly TspSpan[], role: string): NativeNode =>
|
|
236
298
|
keyed(text(spans, { wrap: "none", role }), role);
|
|
237
|
-
const
|
|
238
|
-
|
|
299
|
+
const lockupRow = keyed(
|
|
300
|
+
row(
|
|
239
301
|
[
|
|
240
|
-
art([span("Welcome back!", "strong")], "zeta.welcome.greeting"),
|
|
241
302
|
node(
|
|
242
303
|
"image",
|
|
243
304
|
{
|
|
@@ -249,115 +310,38 @@ export class WelcomeComponent implements Component {
|
|
|
249
310
|
undefined,
|
|
250
311
|
"logo",
|
|
251
312
|
),
|
|
252
|
-
],
|
|
253
|
-
{ align: "center", role: "zeta.welcome.brand" },
|
|
254
|
-
),
|
|
255
|
-
"brand",
|
|
256
|
-
);
|
|
257
|
-
const section = (key: string, label: string, rows: readonly NativeChild[]): NativeNode =>
|
|
258
|
-
keyed(
|
|
259
|
-
col([line([span(label, "dim")], "zeta.welcome.heading"), ...rows], {
|
|
260
|
-
gap: "xs",
|
|
261
|
-
role: `zeta.welcome.${key}`,
|
|
262
|
-
}),
|
|
263
|
-
key,
|
|
264
|
-
);
|
|
265
|
-
const shortcut = (key: string, label: string): NativeNode =>
|
|
266
|
-
keyed(row([kbd(key), line([span(label, "muted")])], { gap: "sm" }), label);
|
|
267
|
-
const info: NativeChild[] = [
|
|
268
|
-
line([span(this.version, "dim mono")], "zeta.welcome.version"),
|
|
269
|
-
section("tips", "Tips", [
|
|
270
|
-
shortcut("#", "prompt actions"),
|
|
271
|
-
shortcut("/", "commands"),
|
|
272
|
-
shortcut("!", "run bash"),
|
|
273
|
-
shortcut("$", "run python"),
|
|
274
|
-
]),
|
|
275
|
-
];
|
|
276
|
-
if (this.lspServers !== null) {
|
|
277
|
-
const lsp: NativeChild[] = [];
|
|
278
|
-
if (this.lspServers.length === 0) lsp.push(line([span("No LSP servers", "dim")], undefined, "none"));
|
|
279
|
-
for (const server of this.lspServers.slice(0, WELCOME_LSP_SLOTS)) {
|
|
280
|
-
const [symbol, token] =
|
|
281
|
-
server.status === "ready"
|
|
282
|
-
? (["status.enabled", "success"] as const)
|
|
283
|
-
: server.status === "available"
|
|
284
|
-
? (["status.enabled", "dim"] as const)
|
|
285
|
-
: server.status === "connecting"
|
|
286
|
-
? (["status.pending", "muted"] as const)
|
|
287
|
-
: (["status.error", "error"] as const);
|
|
288
|
-
lsp.push(
|
|
289
313
|
keyed(
|
|
290
|
-
|
|
314
|
+
col(
|
|
291
315
|
[
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
...server.fileTypes
|
|
295
|
-
.slice(0, 3)
|
|
296
|
-
.map(type => node("badge", { text: type, role: "zeta.welcome.lsp-type" }, undefined, type)),
|
|
316
|
+
art([span("Z e t a", "strong")], "zeta.welcome.wordmark"),
|
|
317
|
+
art([span(`v${this.version}`, "dim mono")], "zeta.welcome.version"),
|
|
297
318
|
],
|
|
298
|
-
{
|
|
319
|
+
{ role: "zeta.welcome.mark" },
|
|
299
320
|
),
|
|
300
|
-
|
|
301
|
-
),
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
info.push(section("lsp", "LSP servers", lsp));
|
|
305
|
-
}
|
|
306
|
-
const recents: NativeChild[] = [];
|
|
307
|
-
if (this.recentSessions.length === 0) recents.push(line([span("No recent sessions", "dim")], undefined, "none"));
|
|
308
|
-
for (const [index, session] of this.recentSessions.slice(0, WELCOME_SESSION_SLOTS).entries()) {
|
|
309
|
-
recents.push(
|
|
310
|
-
keyed(
|
|
311
|
-
row(
|
|
312
|
-
[
|
|
313
|
-
line([span(plainLine(session.name))], "zeta.welcome.session"),
|
|
314
|
-
line([span(session.timeAgo, "dim")], "zeta.welcome.age"),
|
|
315
|
-
],
|
|
316
|
-
{
|
|
317
|
-
gap: "md",
|
|
318
|
-
justify: "between",
|
|
319
|
-
role: "zeta.welcome.recent",
|
|
320
|
-
actions: session.path ? { click: "resume" } : undefined,
|
|
321
|
-
title: session.path ? `Resume ${plainLine(session.name)}` : undefined,
|
|
322
|
-
},
|
|
321
|
+
"mark",
|
|
323
322
|
),
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
info.push(section("recents", "Recent sessions", recents));
|
|
329
|
-
const body: NativeChild[] = [
|
|
330
|
-
keyed(
|
|
331
|
-
row([brand, keyed(col(info, { gap: "md", role: "zeta.welcome.info" }), "info")], {
|
|
332
|
-
align: "start",
|
|
333
|
-
wrap: true,
|
|
334
|
-
role: "zeta.welcome.grid",
|
|
335
|
-
}),
|
|
336
|
-
"grid",
|
|
323
|
+
],
|
|
324
|
+
{ align: "center", gap: "md", role: "zeta.welcome.lockup" },
|
|
337
325
|
),
|
|
338
|
-
|
|
326
|
+
"lockup",
|
|
327
|
+
);
|
|
328
|
+
const body: NativeChild[] = [lockupRow];
|
|
339
329
|
if (tip) {
|
|
340
|
-
const
|
|
341
|
-
const tipText = plainLine(expandTipKeys(isNew ? tip.replace(NEW_TIP_MARKER, "") : tip));
|
|
330
|
+
const tipText = compactText(tipSpans(expandTipKeys(tip.text)));
|
|
342
331
|
const tipRow: NativeChild[] = [
|
|
343
332
|
node("icon", { name: "lightbulb", role: "zeta.welcome.tip-icon" }),
|
|
344
333
|
text(tipText, { wrap: "word", role: "zeta.welcome.tip-text" }),
|
|
345
334
|
];
|
|
346
|
-
if (isNew)
|
|
335
|
+
if (tip.isNew) {
|
|
336
|
+
tipRow.push(node("shimmer", { text: tuiText("welcomeNewTag", NEW_TAG_TEXT), role: "zeta.welcome.new" }));
|
|
337
|
+
}
|
|
347
338
|
body.push(node("row", { gap: "sm", align: "start", role: "zeta.welcome.tip" }, tipRow, "tip"));
|
|
348
339
|
}
|
|
349
|
-
// No head row or chevron: the card is the hero; the version sits
|
|
340
|
+
// No head row or chevron: the card is the hero; the version sits under the wordmark.
|
|
350
341
|
const described = card({ role: "zeta.welcome" }, body);
|
|
351
|
-
this.#native = { tip, node: described };
|
|
342
|
+
this.#native = { lang, tip: tip?.text, node: described };
|
|
352
343
|
return described;
|
|
353
344
|
}
|
|
354
|
-
/** A click on a recent session resumes it. */
|
|
355
|
-
handleNativeEvent(event: NativeUiEvent): void {
|
|
356
|
-
if (event.type !== "action" || event.act !== "resume") return;
|
|
357
|
-
const index = Number(/\/s(\d+)$/.exec(event.key)?.[1]);
|
|
358
|
-
const path = this.recentSessions[index]?.path;
|
|
359
|
-
if (path) runTranscriptAction({ act: "resume", path });
|
|
360
|
-
}
|
|
361
345
|
|
|
362
346
|
/** The intro keeps the welcome block mutable; settling lets it retire to history. */
|
|
363
347
|
isTranscriptBlockFinalized(): boolean {
|
|
@@ -425,25 +409,10 @@ export class WelcomeComponent implements Component {
|
|
|
425
409
|
this.invalidate();
|
|
426
410
|
}
|
|
427
411
|
|
|
428
|
-
setModel(modelName: string, providerName: string): void {
|
|
429
|
-
this.modelName = modelName;
|
|
430
|
-
this.providerName = providerName;
|
|
431
|
-
this.invalidate();
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
setRecentSessions(sessions: RecentSession[]): void {
|
|
435
|
-
this.recentSessions = sessions;
|
|
436
|
-
this.invalidate();
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
setLspServers(servers: LspServerInfo[] | null): void {
|
|
440
|
-
this.lspServers = servers;
|
|
441
|
-
this.invalidate();
|
|
442
|
-
}
|
|
443
|
-
|
|
444
412
|
render(termWidth: number): readonly string[] {
|
|
445
413
|
const animating = this.#animStart != null;
|
|
446
|
-
|
|
414
|
+
const lang = tuiText("welcomeBack", "Welcome back!");
|
|
415
|
+
if (!animating && this.#cachedLines && this.#cachedWidth === termWidth && this.#cachedLang === lang) {
|
|
447
416
|
return this.#cachedLines;
|
|
448
417
|
}
|
|
449
418
|
const lines = this.#renderLines(termWidth);
|
|
@@ -453,223 +422,39 @@ export class WelcomeComponent implements Component {
|
|
|
453
422
|
} else {
|
|
454
423
|
this.#cachedLines = lines;
|
|
455
424
|
this.#cachedWidth = termWidth;
|
|
425
|
+
this.#cachedLang = lang;
|
|
456
426
|
}
|
|
457
427
|
return lines;
|
|
458
428
|
}
|
|
459
429
|
|
|
460
430
|
#renderLines(termWidth: number): string[] {
|
|
461
|
-
//
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const
|
|
468
|
-
const
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
// Dynamic model/provider labels are truncated inside the fixed column.
|
|
472
|
-
// Letting them influence the responsive breakpoint changes the box height
|
|
473
|
-
// when authoritative session data replaces the empty prepaint labels.
|
|
474
|
-
const leftMinContentWidth = Math.max(minLeftCol, visibleWidth("Welcome back!"));
|
|
475
|
-
const desiredLeftCol = Math.max(
|
|
476
|
-
Math.min(preferredLeftCol, Math.max(minLeftCol, Math.floor(dualContentWidth * 0.35))),
|
|
477
|
-
leftMinContentWidth,
|
|
478
|
-
);
|
|
479
|
-
const dualLeftCol =
|
|
480
|
-
dualContentWidth >= minRightCol + 1
|
|
481
|
-
? Math.min(desiredLeftCol, dualContentWidth - minRightCol)
|
|
482
|
-
: Math.max(1, dualContentWidth - 1);
|
|
483
|
-
const dualRightCol = Math.max(1, dualContentWidth - dualLeftCol);
|
|
484
|
-
const showRightColumn = dualLeftCol >= leftMinContentWidth && dualRightCol >= minRightCol;
|
|
485
|
-
const leftCol = showRightColumn ? dualLeftCol : boxWidth - 2;
|
|
486
|
-
const rightCol = showRightColumn ? dualRightCol : 0;
|
|
487
|
-
|
|
488
|
-
// Logo: pick a frame from the intro animation if active, else the resting frame.
|
|
489
|
-
const logoColored = this.#currentLogoFrame();
|
|
490
|
-
|
|
491
|
-
// Left column - centered content
|
|
492
|
-
const leftLines = [
|
|
493
|
-
"",
|
|
494
|
-
this.#centerText(theme.bold("Welcome back!"), leftCol),
|
|
495
|
-
"",
|
|
496
|
-
...logoColored.map(l => this.#centerText(l, leftCol)),
|
|
497
|
-
"",
|
|
498
|
-
this.#centerText(theme.fg("muted", this.modelName), leftCol),
|
|
499
|
-
this.#centerText(theme.fg("borderMuted", this.providerName), leftCol),
|
|
500
|
-
];
|
|
501
|
-
|
|
502
|
-
// Right column separator
|
|
503
|
-
const separatorWidth = Math.max(0, rightCol - 2); // padding on each side
|
|
504
|
-
const separator = ` ${theme.fg("dim", theme.boxRound.horizontal.repeat(separatorWidth))}`;
|
|
505
|
-
|
|
506
|
-
// Recent sessions content
|
|
507
|
-
const sessionLines: string[] = [];
|
|
508
|
-
if (this.recentSessions.length === 0) {
|
|
509
|
-
sessionLines.push(` ${theme.fg("dim", "No recent sessions")}`);
|
|
510
|
-
} else {
|
|
511
|
-
// Reserve width for the bullet prefix (" • ") and the trailing " (timeAgo)"
|
|
512
|
-
// so the relative time is never the part that gets truncated. The name
|
|
513
|
-
// absorbs whatever space is left.
|
|
514
|
-
const bulletPrefix = ` ${theme.md.bullet} `;
|
|
515
|
-
const prefixWidth = visibleWidth(bulletPrefix);
|
|
516
|
-
for (const session of this.recentSessions.slice(0, WELCOME_SESSION_SLOTS)) {
|
|
517
|
-
const timeSuffixRaw = ` (${session.timeAgo})`;
|
|
518
|
-
const timeWidth = visibleWidth(timeSuffixRaw);
|
|
519
|
-
const nameBudget = Math.max(1, rightCol - prefixWidth - timeWidth);
|
|
520
|
-
const nameVis = visibleWidth(session.name);
|
|
521
|
-
const name = nameVis > nameBudget ? truncateToWidth(session.name, nameBudget) : session.name;
|
|
522
|
-
sessionLines.push(
|
|
523
|
-
`${theme.fg("dim", bulletPrefix)}${theme.fg("muted", name)}${theme.fg("dim", timeSuffixRaw)}`,
|
|
524
|
-
);
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
// Pad to the fixed slot count so the box height doesn't depend on session count.
|
|
528
|
-
while (sessionLines.length < WELCOME_SESSION_SLOTS) {
|
|
529
|
-
sessionLines.push("");
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// Right column
|
|
533
|
-
const rightLines = [
|
|
534
|
-
` ${theme.bold(theme.fg("accent", "Tips"))}`,
|
|
535
|
-
` ${theme.fg("dim", "#")}${theme.fg("muted", " for prompt actions")}`,
|
|
536
|
-
` ${theme.fg("dim", "/")}${theme.fg("muted", " for commands")}`,
|
|
537
|
-
` ${theme.fg("dim", "!")}${theme.fg("muted", " to run bash")}`,
|
|
538
|
-
` ${theme.fg("dim", "$")}${theme.fg("muted", " to run python")}`,
|
|
539
|
-
...this.#renderLspSection(separator),
|
|
540
|
-
separator,
|
|
541
|
-
` ${theme.bold(theme.fg("accent", "Recent sessions"))}`,
|
|
542
|
-
...sessionLines,
|
|
543
|
-
"",
|
|
544
|
-
];
|
|
545
|
-
|
|
546
|
-
// Border characters (dim)
|
|
547
|
-
const hChar = theme.boxRound.horizontal;
|
|
548
|
-
const h = theme.fg("dim", hChar);
|
|
549
|
-
const v = theme.fg("dim", theme.boxRound.vertical);
|
|
550
|
-
const tl = theme.fg("dim", theme.boxRound.topLeft);
|
|
551
|
-
const tr = theme.fg("dim", theme.boxRound.topRight);
|
|
552
|
-
const bl = theme.fg("dim", theme.boxRound.bottomLeft);
|
|
553
|
-
const br = theme.fg("dim", theme.boxRound.bottomRight);
|
|
554
|
-
|
|
555
|
-
const lines: string[] = [];
|
|
556
|
-
|
|
557
|
-
// Top border with embedded title
|
|
558
|
-
const title = ` ${APP_NAME} v${this.version} `;
|
|
559
|
-
const titlePrefixRaw = hChar.repeat(3);
|
|
560
|
-
const titleStyled = theme.fg("dim", titlePrefixRaw) + theme.fg("muted", title);
|
|
561
|
-
const titleVisLen = visibleWidth(titlePrefixRaw) + visibleWidth(title);
|
|
562
|
-
const titleSpace = boxWidth - 2;
|
|
563
|
-
if (titleVisLen >= titleSpace) {
|
|
564
|
-
lines.push(tl + truncateToWidth(titleStyled, titleSpace) + tr);
|
|
565
|
-
} else {
|
|
566
|
-
const afterTitle = titleSpace - titleVisLen;
|
|
567
|
-
lines.push(tl + titleStyled + theme.fg("dim", hChar.repeat(afterTitle)) + tr);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// Content rows
|
|
571
|
-
const maxRows = showRightColumn ? Math.max(leftLines.length, rightLines.length) : leftLines.length;
|
|
572
|
-
for (let i = 0; i < maxRows; i++) {
|
|
573
|
-
const left = this.#fitToWidth(leftLines[i] ?? "", leftCol);
|
|
574
|
-
if (showRightColumn) {
|
|
575
|
-
const right = this.#fitToWidth(rightLines[i] ?? "", rightCol);
|
|
576
|
-
lines.push(v + left + v + right + v);
|
|
577
|
-
} else {
|
|
578
|
-
lines.push(v + left + v);
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
// Bottom border
|
|
582
|
-
if (showRightColumn) {
|
|
583
|
-
lines.push(bl + h.repeat(leftCol) + theme.fg("dim", theme.boxRound.teeUp) + h.repeat(rightCol) + br);
|
|
584
|
-
} else {
|
|
585
|
-
lines.push(bl + h.repeat(leftCol) + br);
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// Randomly picked tip, rendered directly beneath the box.
|
|
589
|
-
// The banner centers every tip line, so the tip drops out entirely on
|
|
590
|
-
// narrow boxes rather than wrapping into unreadable slivers.
|
|
591
|
-
if (termWidth >= 16) lines.push(...this.#renderTip(boxWidth));
|
|
592
|
-
|
|
431
|
+
// Content keeps a column clear on each side; everything centers in the full width.
|
|
432
|
+
const room = termWidth - 2;
|
|
433
|
+
if (room < 4) return [];
|
|
434
|
+
const logo = this.#currentLogoFrame();
|
|
435
|
+
const version = theme.fg("dim", `v${this.version}`);
|
|
436
|
+
const lockupWidth = LOGO_WIDTH + LOCKUP_GAP + Math.max(WORDMARK_WIDTH, visibleWidth(version));
|
|
437
|
+
const art = room >= lockupWidth ? lockup(logo, version) : room >= LOGO_WIDTH ? logo : [];
|
|
438
|
+
const lines = centerBlock(art, termWidth);
|
|
439
|
+
const tip = termWidth >= TIP_MIN_COLUMNS ? this.#renderTip(room) : [];
|
|
440
|
+
if (tip.length > 0) lines.push("", ...tip.flatMap(line => centerBlock([line], termWidth)));
|
|
593
441
|
return lines;
|
|
594
442
|
}
|
|
595
443
|
|
|
596
|
-
/** Right-column LSP rows padded to a fixed height; empty when LSP is disabled. */
|
|
597
|
-
#renderLspSection(separator: string): string[] {
|
|
598
|
-
if (this.lspServers === null) return [];
|
|
599
|
-
const lspLines: string[] = [];
|
|
600
|
-
if (this.lspServers.length === 0) {
|
|
601
|
-
lspLines.push(` ${theme.fg("dim", "No LSP servers")}`);
|
|
602
|
-
} else {
|
|
603
|
-
for (const server of this.lspServers.slice(0, WELCOME_LSP_SLOTS)) {
|
|
604
|
-
const icon =
|
|
605
|
-
server.status === "ready"
|
|
606
|
-
? theme.styledSymbol("status.enabled", "success")
|
|
607
|
-
: server.status === "available"
|
|
608
|
-
? theme.styledSymbol("status.enabled", "dim")
|
|
609
|
-
: server.status === "connecting"
|
|
610
|
-
? theme.styledSymbol("status.pending", "muted")
|
|
611
|
-
: theme.styledSymbol("status.error", "error");
|
|
612
|
-
const exts = server.fileTypes.slice(0, 3).join(" ");
|
|
613
|
-
lspLines.push(` ${icon} ${theme.fg("muted", server.name)} ${theme.fg("dim", exts)}`);
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
// Pad to the fixed slot count so the box height doesn't depend on server count.
|
|
617
|
-
while (lspLines.length < WELCOME_LSP_SLOTS) {
|
|
618
|
-
lspLines.push("");
|
|
619
|
-
}
|
|
620
|
-
return [separator, ` ${theme.bold(theme.fg("accent", "LSP Servers"))}`, ...lspLines];
|
|
621
|
-
}
|
|
622
|
-
|
|
623
444
|
/**
|
|
624
|
-
*
|
|
625
|
-
*
|
|
626
|
-
* when no tip is available or the box is too narrow to be useful.
|
|
445
|
+
* The tip of the session wrapped to {@link TIP_MEASURE} columns (fewer when
|
|
446
|
+
* `room` is narrower); `[]` when there is none or no room for it.
|
|
627
447
|
*/
|
|
628
|
-
#renderTip(
|
|
629
|
-
const
|
|
630
|
-
if (!
|
|
631
|
-
// A trailing "[NEW]" marker paints an animated rainbow
|
|
632
|
-
//
|
|
633
|
-
//
|
|
448
|
+
#renderTip(room: number): string[] {
|
|
449
|
+
const raw = this.tip;
|
|
450
|
+
if (!raw) return [];
|
|
451
|
+
// A trailing "[NEW]" marker paints an animated rainbow tag. Derive its
|
|
452
|
+
// hue phase from wall-clock time so it shimmers across the welcome intro's
|
|
453
|
+
// re-render frames, then settles into a still rainbow once the banner
|
|
634
454
|
// caches its resting frame. Non-"[NEW]" tips ignore the phase entirely.
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
/** Center text within a given width */
|
|
640
|
-
#centerText(text: string, width: number): string {
|
|
641
|
-
const visLen = visibleWidth(text);
|
|
642
|
-
if (visLen >= width) {
|
|
643
|
-
return truncateToWidth(text, width);
|
|
644
|
-
}
|
|
645
|
-
const leftPad = Math.floor((width - visLen) / 2);
|
|
646
|
-
const rightPad = width - visLen - leftPad;
|
|
647
|
-
return padding(leftPad) + text + padding(rightPad);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
/** Fit string to exact width with ANSI-aware truncation/padding */
|
|
651
|
-
#fitToWidth(str: string, width: number): string {
|
|
652
|
-
const visLen = visibleWidth(str);
|
|
653
|
-
if (visLen > width) {
|
|
654
|
-
const ellipsis = "…";
|
|
655
|
-
const ellipsisWidth = visibleWidth(ellipsis);
|
|
656
|
-
const maxWidth = Math.max(0, width - ellipsisWidth);
|
|
657
|
-
let truncated = "";
|
|
658
|
-
let currentWidth = 0;
|
|
659
|
-
let inEscape = false;
|
|
660
|
-
for (const char of str) {
|
|
661
|
-
if (char === "\x1b") inEscape = true;
|
|
662
|
-
if (inEscape) {
|
|
663
|
-
truncated += char;
|
|
664
|
-
if (char === "m") inEscape = false;
|
|
665
|
-
} else if (currentWidth < maxWidth) {
|
|
666
|
-
truncated += char;
|
|
667
|
-
currentWidth++;
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
return `${truncated}${ellipsis}`;
|
|
671
|
-
}
|
|
672
|
-
return str + padding(width - visLen);
|
|
455
|
+
const tip = resolveTip(raw);
|
|
456
|
+
const phase = tip.isNew ? performance.now() / NEW_GLOW_PERIOD_MS : 0;
|
|
457
|
+
return renderWelcomeTip(tip.text, Math.min(room, TIP_MEASURE), phase, tip.isNew);
|
|
673
458
|
}
|
|
674
459
|
|
|
675
460
|
/** Pick the logo frame for the current intro phase, or the resting frame. */
|
|
@@ -682,7 +467,46 @@ export class WelcomeComponent implements Component {
|
|
|
682
467
|
}
|
|
683
468
|
|
|
684
469
|
/** Block-grid brand mark shared by the welcome and setup surfaces. */
|
|
685
|
-
export const ZETA_LOGO = ["
|
|
470
|
+
export const ZETA_LOGO = ["████████████", " ████████ ", " ████████ ", " ████████▒▒▒", "████████████"];
|
|
471
|
+
|
|
472
|
+
/** Columns of {@link ZETA_LOGO}. */
|
|
473
|
+
const LOGO_WIDTH = Math.max(...ZETA_LOGO.map(row => row.length));
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* The `Zeta` wordmark in half-blocks, set beside {@link ZETA_LOGO} from its
|
|
477
|
+
* second row: the `Z` is a solid zig-zag closed by a light step, `e`/`a` sit on
|
|
478
|
+
* the x-height, the `t` ascender rises above it, and the version takes the
|
|
479
|
+
* fifth row.
|
|
480
|
+
*/
|
|
481
|
+
const WORDMARK = ["███▀ ▄ ", " ██ ███▄ ███ ▄██▄", "▄▄██ █▄▄ █▄ █▄▄█"];
|
|
482
|
+
|
|
483
|
+
/** Columns of {@link WORDMARK}. */
|
|
484
|
+
const WORDMARK_WIDTH = Math.max(...WORDMARK.map(row => row.length));
|
|
485
|
+
|
|
486
|
+
/** Columns between the logo and the wordmark. */
|
|
487
|
+
const LOCKUP_GAP = 4;
|
|
488
|
+
|
|
489
|
+
/** Widest a welcome tip wraps, so a long one stays a centered paragraph. */
|
|
490
|
+
const TIP_MEASURE = 72;
|
|
491
|
+
|
|
492
|
+
/** Narrowest terminal that still shows the tip; below it the banner is the logo alone. */
|
|
493
|
+
const TIP_MIN_COLUMNS = 50;
|
|
494
|
+
|
|
495
|
+
/** Logo frame `logo` with the wordmark beside it and `version` (styled) under the wordmark. */
|
|
496
|
+
function lockup(logo: readonly string[], version: string): string[] {
|
|
497
|
+
const beside = ["", ...WORDMARK.map(row => theme.bold(theme.fg("text", row))), version];
|
|
498
|
+
return logo.map((row, index) => `${row}${padding(LOCKUP_GAP)}${beside[index] ?? ""}`);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* `lines` indented as one block whose widest line is centered in `width`
|
|
503
|
+
* columns; pass a single line to center it on its own.
|
|
504
|
+
*/
|
|
505
|
+
function centerBlock(lines: readonly string[], width: number): string[] {
|
|
506
|
+
const widest = lines.reduce((max, line) => Math.max(max, visibleWidth(line)), 0);
|
|
507
|
+
const indent = padding(Math.max(0, Math.floor((width - widest) / 2)));
|
|
508
|
+
return lines.map(line => indent + line);
|
|
509
|
+
}
|
|
686
510
|
|
|
687
511
|
/** The block-grid brand mark as accent lines; `shimmer` declares the terminal-clocked shine sweep. */
|
|
688
512
|
export function logoNode(lines: readonly string[], shimmer: boolean): NativeNode {
|