@linxiraos/pi-tui 1.2.1 → 1.3.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 +188 -125
- package/README.md +36 -0
- package/THIRD-PARTY-NOTICES.txt +55 -5
- package/dist/types/app-keybindings.d.ts +18 -8
- package/dist/types/apps/git/diff-pane.d.ts +5 -0
- package/dist/types/apps/git/git-tui.d.ts +16 -2
- package/dist/types/apps/git/state.d.ts +11 -0
- package/dist/types/autocomplete.d.ts +2 -0
- package/dist/types/charts/chart-svg.d.ts +7 -13
- package/dist/types/chat/advisor-message.d.ts +4 -1
- package/dist/types/chat/assistant-message.d.ts +9 -4
- package/dist/types/chat/figure-markdown.d.ts +7 -2
- package/dist/types/chat/messages.d.ts +3 -1
- package/dist/types/chat/read-tool-group.d.ts +2 -2
- package/dist/types/chat/table-chart.d.ts +11 -15
- package/dist/types/chat/tool-execution.d.ts +32 -20
- package/dist/types/chat/user-message.d.ts +3 -2
- package/dist/types/chrome/countdown-timer.d.ts +3 -2
- package/dist/types/chrome/diff.d.ts +2 -3
- package/dist/types/chrome/transcript-container.d.ts +2 -0
- package/dist/types/components/image.d.ts +2 -0
- package/dist/types/components/markdown.d.ts +49 -3
- package/dist/types/components/text.d.ts +2 -0
- package/dist/types/debug-server.d.ts +2 -0
- package/dist/types/keybinding-matchers.d.ts +6 -0
- package/dist/types/multiplexers/tmux.d.ts +1 -0
- package/dist/types/multiplexers/types.d.ts +6 -0
- package/dist/types/native/backend.d.ts +5 -4
- package/dist/types/native/blobs.d.ts +24 -12
- package/dist/types/overlays/agent-hub-projection.d.ts +10 -5
- package/dist/types/overlays/annotation-overlay.d.ts +19 -0
- package/dist/types/overlays/approval-dialog.d.ts +79 -0
- package/dist/types/overlays/ask-dialog.d.ts +11 -6
- package/dist/types/overlays/btw-history-panel.d.ts +2 -6
- package/dist/types/overlays/hook-editor.d.ts +7 -2
- package/dist/types/overlays/hook-input.d.ts +12 -4
- package/dist/types/overlays/job-tree.d.ts +50 -0
- package/dist/types/overlays/jobs-panel.d.ts +25 -11
- package/dist/types/overlays/login-dialog.d.ts +8 -3
- package/dist/types/overlays/mcp-add-wizard.d.ts +9 -1
- package/dist/types/overlays/model-browser.d.ts +6 -1
- package/dist/types/overlays/model-picker.d.ts +2 -0
- package/dist/types/overlays/pause-screen.d.ts +5 -0
- package/dist/types/overlays/plan-review-overlay.d.ts +4 -0
- package/dist/types/overlays/report-panel.d.ts +5 -1
- package/dist/types/overlays/reset-usage-selector.d.ts +7 -5
- package/dist/types/overlays/session-account-selector.d.ts +4 -0
- package/dist/types/overlays/session-info-overlay.d.ts +7 -3
- package/dist/types/overlays/session-selector.d.ts +3 -1
- package/dist/types/overlays/usage-dashboard.d.ts +6 -6
- package/dist/types/prompt/composer.d.ts +21 -20
- package/dist/types/prompt/custom-editor.d.ts +20 -10
- package/dist/types/prompt/image-references.d.ts +22 -0
- package/dist/types/prompt/internal-url-autocomplete.d.ts +1 -0
- package/dist/types/prompt/macos-spelling.d.ts +2 -1
- package/dist/types/prompt/prose-gate.d.ts +3 -0
- package/dist/types/prompt/queued-messages.d.ts +1 -1
- package/dist/types/prompt/typing-word.d.ts +25 -0
- package/dist/types/prompt/welcome.d.ts +26 -6
- package/dist/types/render/hanging-wrap.d.ts +7 -0
- package/dist/types/render/hyperlink.d.ts +2 -0
- package/dist/types/render/render-utils.d.ts +9 -1
- package/dist/types/render/utils.d.ts +1 -1
- package/dist/types/setup/scenes/splash.d.ts +6 -5
- package/dist/types/status-line/context-usage.d.ts +4 -1
- package/dist/types/status-line/host.d.ts +6 -4
- package/dist/types/status-line/startup.d.ts +6 -6
- package/dist/types/status-line/types.d.ts +3 -0
- package/dist/types/stdin-buffer.d.ts +7 -0
- package/dist/types/terminal-capabilities.d.ts +16 -1
- package/dist/types/terminal-multiplexer.d.ts +7 -0
- package/dist/types/terminal.d.ts +8 -4
- package/dist/types/theme/symbols.d.ts +1 -1
- package/dist/types/theme/theme.d.ts +11 -0
- package/dist/types/thinking.d.ts +5 -0
- package/dist/types/tmux.d.ts +39 -4
- package/dist/types/tools/ast-edit.d.ts +2 -2
- package/dist/types/tools/ast-grep.d.ts +2 -2
- package/dist/types/tools/bash.d.ts +6 -1
- package/dist/types/tools/edit.d.ts +2 -0
- package/dist/types/tools/eval.d.ts +53 -4
- package/dist/types/tools/find.d.ts +14 -4
- package/dist/types/tools/glob.d.ts +3 -3
- package/dist/types/tools/grep.d.ts +2 -2
- package/dist/types/tools/grouped-file-output.d.ts +9 -4
- package/dist/types/tools/native-view.d.ts +1 -1
- package/dist/types/tools/read.d.ts +32 -10
- package/dist/types/tools/renderer.d.ts +2 -0
- package/dist/types/tools/task.d.ts +31 -0
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/tui.d.ts +20 -3
- package/package.json +10 -10
- package/src/app-keybindings.d.ts +18 -8
- package/src/app-keybindings.ts +20 -8
- package/src/apps/autoresearch-dashboard.ts +444 -27
- package/src/apps/debug/raw-sse.ts +2 -2
- package/src/apps/git/diff-pane.ts +39 -41
- package/src/apps/git/git-tui.ts +80 -14
- package/src/apps/git/sidebar.ts +10 -1
- package/src/apps/git/state.ts +12 -0
- package/src/autocomplete.d.ts +2 -0
- package/src/autocomplete.ts +21 -19
- package/src/bracketed-paste.ts +9 -2
- package/src/charts/chart-svg.d.ts +7 -13
- package/src/charts/chart-svg.ts +19 -22
- package/src/chat/advisor-message.d.ts +4 -1
- package/src/chat/advisor-message.ts +77 -24
- package/src/chat/assistant-message.d.ts +9 -4
- package/src/chat/assistant-message.ts +127 -31
- package/src/chat/bash-execution.ts +7 -2
- package/src/chat/figure-markdown.d.ts +7 -2
- package/src/chat/figure-markdown.ts +10 -3
- package/src/chat/image-loading.ts +9 -6
- package/src/chat/messages.d.ts +3 -1
- package/src/chat/messages.ts +4 -1
- package/src/chat/read-tool-group.d.ts +2 -2
- package/src/chat/read-tool-group.ts +97 -42
- package/src/chat/svg-figure.ts +3 -1
- package/src/chat/svg-source.ts +6 -3
- package/src/chat/table-chart.d.ts +11 -15
- package/src/chat/table-chart.ts +24 -57
- package/src/chat/tool-execution.d.ts +32 -20
- package/src/chat/tool-execution.ts +103 -36
- package/src/chat/user-message.d.ts +3 -2
- package/src/chat/user-message.ts +54 -34
- package/src/chrome/countdown-timer.ts +3 -3
- package/src/chrome/diff.d.ts +2 -3
- package/src/chrome/diff.ts +4 -9
- package/src/chrome/transcript-container.d.ts +2 -0
- package/src/chrome/transcript-container.ts +212 -28
- package/src/components/editor.ts +12 -12
- package/src/components/image.d.ts +2 -0
- package/src/components/image.ts +54 -20
- package/src/components/markdown.d.ts +49 -3
- package/src/components/markdown.ts +461 -11
- package/src/components/settings-list.ts +2 -4
- package/src/components/text.d.ts +2 -0
- package/src/components/text.ts +11 -1
- package/src/debug-server.d.ts +2 -0
- package/src/debug-server.ts +51 -69
- package/src/hotkeys-markdown.ts +2 -0
- package/src/keybinding-matchers.d.ts +6 -0
- package/src/keybinding-matchers.ts +19 -0
- package/src/multiplexers/tmux.d.ts +1 -0
- package/src/multiplexers/tmux.ts +3 -0
- package/src/multiplexers/types.d.ts +6 -0
- package/src/multiplexers/types.ts +6 -0
- package/src/native/backend.d.ts +5 -4
- package/src/native/backend.ts +13 -5
- package/src/native/blobs.d.ts +24 -12
- package/src/native/blobs.ts +56 -7
- package/src/native/reconcile.ts +43 -21
- package/src/overlays/advisor-config.ts +259 -8
- package/src/overlays/agent-hub-projection.d.ts +10 -5
- package/src/overlays/agent-hub-projection.ts +63 -12
- package/src/overlays/annotation-overlay.ts +422 -168
- package/src/overlays/approval-dialog.ts +389 -0
- package/src/overlays/ask-dialog.ts +244 -105
- package/src/overlays/btw-history-panel.ts +208 -147
- package/src/overlays/extensions/extension-list.ts +4 -6
- package/src/overlays/hook-editor.ts +90 -32
- package/src/overlays/hook-input.ts +105 -19
- package/src/overlays/job-tree.ts +177 -0
- package/src/overlays/jobs-panel.ts +222 -142
- package/src/overlays/login-dialog.ts +201 -91
- package/src/overlays/mcp-add-wizard.ts +562 -279
- package/src/overlays/model-browser.ts +32 -7
- package/src/overlays/model-hub.ts +4 -10
- package/src/overlays/model-picker.ts +58 -21
- package/src/overlays/pause-screen.ts +81 -23
- package/src/overlays/plan-review-overlay.ts +16 -0
- package/src/overlays/plugin-selector.ts +17 -1
- package/src/overlays/queue-mode-selector.ts +10 -0
- package/src/overlays/report-panel.ts +6 -2
- package/src/overlays/reset-usage-selector.ts +130 -21
- package/src/overlays/session-account-selector.ts +36 -0
- package/src/overlays/session-info-overlay.ts +199 -76
- package/src/overlays/session-selector.ts +15 -10
- package/src/overlays/settings-selector.ts +3 -0
- package/src/overlays/show-images-selector.ts +1 -0
- package/src/overlays/snapcompact-shape-preview.ts +13 -6
- package/src/overlays/theme-selector.ts +6 -1
- package/src/overlays/thinking-selector.ts +8 -4
- package/src/overlays/tree-selector.ts +117 -2
- package/src/overlays/usage-dashboard.ts +498 -144
- package/src/prompt/composer-cache.ts +1 -1
- package/src/prompt/composer.ts +194 -74
- package/src/prompt/custom-editor.ts +168 -65
- package/src/prompt/image-references.d.ts +22 -0
- package/src/prompt/image-references.ts +79 -2
- package/src/prompt/internal-url-autocomplete.ts +1 -0
- package/src/prompt/macos-spelling.ts +28 -5
- package/src/prompt/prose-gate.ts +3 -0
- package/src/prompt/queued-messages.ts +9 -2
- package/src/prompt/tips.txt +1 -2
- package/src/prompt/typing-word.ts +59 -0
- package/src/prompt/welcome.ts +97 -45
- package/src/render/code-cell.ts +18 -5
- package/src/render/hanging-wrap.d.ts +7 -0
- package/src/render/hanging-wrap.ts +58 -0
- package/src/render/hyperlink.d.ts +2 -0
- package/src/render/hyperlink.ts +5 -0
- package/src/render/render-utils.d.ts +9 -1
- package/src/render/render-utils.ts +16 -2
- package/src/render/utils.d.ts +1 -1
- package/src/render/utils.ts +1 -1
- package/src/render/width-aware-text.ts +6 -5
- package/src/setup/scenes/splash.ts +22 -8
- package/src/status-line/component.ts +23 -10
- package/src/status-line/context-usage.ts +176 -73
- package/src/status-line/footer.ts +18 -35
- package/src/status-line/host.ts +6 -1
- package/src/status-line/segments.ts +16 -5
- package/src/status-line/startup.ts +6 -6
- package/src/status-line/types.ts +1 -0
- package/src/stdin-buffer.d.ts +7 -0
- package/src/stdin-buffer.ts +11 -1
- package/src/terminal-capabilities.d.ts +16 -1
- package/src/terminal-capabilities.ts +66 -28
- package/src/terminal-multiplexer.d.ts +7 -0
- package/src/terminal-multiplexer.ts +14 -0
- package/src/terminal.d.ts +8 -4
- package/src/terminal.ts +160 -46
- package/src/theme/symbols.d.ts +1 -1
- package/src/theme/symbols.ts +1 -0
- package/src/theme/theme-class.ts +1 -0
- package/src/theme/theme.d.ts +11 -0
- package/src/theme/theme.ts +46 -14
- package/src/thinking.d.ts +5 -0
- package/src/thinking.ts +9 -1
- package/src/tmux.d.ts +39 -4
- package/src/tmux.ts +110 -16
- package/src/tools/ask.ts +20 -9
- package/src/tools/ast-edit.d.ts +2 -2
- package/src/tools/ast-edit.ts +30 -7
- package/src/tools/ast-grep.d.ts +2 -2
- package/src/tools/ast-grep.ts +39 -35
- package/src/tools/bash.d.ts +6 -1
- package/src/tools/bash.ts +15 -9
- package/src/tools/debug.ts +2 -1
- package/src/tools/default-renderer.ts +3 -2
- package/src/tools/edit.d.ts +2 -0
- package/src/tools/edit.ts +100 -21
- package/src/tools/eval.d.ts +53 -4
- package/src/tools/eval.ts +173 -137
- package/src/tools/find.d.ts +14 -4
- package/src/tools/find.ts +71 -18
- package/src/tools/glob.d.ts +3 -3
- package/src/tools/glob.ts +51 -17
- package/src/tools/grep.d.ts +2 -2
- package/src/tools/grep.ts +99 -22
- package/src/tools/grouped-file-output.d.ts +9 -4
- package/src/tools/grouped-file-output.ts +21 -6
- package/src/tools/lsp.ts +30 -18
- package/src/tools/mcp.ts +2 -2
- package/src/tools/native-view.d.ts +1 -1
- package/src/tools/native-view.ts +2 -2
- package/src/tools/read.d.ts +32 -10
- package/src/tools/read.ts +89 -43
- package/src/tools/renderer.d.ts +2 -0
- package/src/tools/renderer.ts +2 -0
- package/src/tools/task.d.ts +31 -0
- package/src/tools/task.ts +40 -7
- package/src/tools/todo.ts +3 -2
- package/src/tools/wait.ts +16 -30
- package/src/tools/web-search.ts +2 -1
- package/src/tools/write.d.ts +2 -0
- package/src/tools/write.ts +36 -12
- package/src/tools/yield.ts +2 -2
- package/src/tui.d.ts +20 -3
- package/src/tui.ts +140 -36
- package/src/utils.ts +5 -3
- package/dist/types/tools/eval-format/code-mode-declarations.d.ts +0 -5
- package/src/tools/eval-format/code-mode-declarations.ts +0 -61
|
@@ -18,7 +18,7 @@ export interface QueuedMessageGroup {
|
|
|
18
18
|
* Messages queued while the agent works, above the editor. ANSI: a heading
|
|
19
19
|
* per group, numbered dim rows and the dequeue hint. Natively (§8.1): a
|
|
20
20
|
* stack of pills, each `corner-down-right` · the text · `⌥↑ Edit`, the total
|
|
21
|
-
*
|
|
21
|
+
* (`N queued`, not a position) in the first pill when more than one waits; the edit control sends
|
|
22
22
|
* `queue.edit`, answered like the dequeue key.
|
|
23
23
|
*/
|
|
24
24
|
export class QueuedMessagesBand extends Container {
|
|
@@ -63,7 +63,14 @@ export class QueuedMessagesBand extends Container {
|
|
|
63
63
|
),
|
|
64
64
|
];
|
|
65
65
|
if (pills.length === 0 && count > 1) {
|
|
66
|
-
children.push(
|
|
66
|
+
children.push(
|
|
67
|
+
node(
|
|
68
|
+
"badge",
|
|
69
|
+
{ text: `${count} queued`, title: `${count} messages queued`, role: "zeta.queue.count" },
|
|
70
|
+
undefined,
|
|
71
|
+
"count",
|
|
72
|
+
),
|
|
73
|
+
);
|
|
67
74
|
}
|
|
68
75
|
children.push(
|
|
69
76
|
node(
|
package/src/prompt/tips.txt
CHANGED
|
@@ -29,5 +29,4 @@ Turn on `/advisor` to attach a second model that reviews every turn and quietly
|
|
|
29
29
|
Try starting your prompt with a ->, and writing a list (1. Do X, 2. Do Y)
|
|
30
30
|
Press {key:shift+tab} to cycle through reasoning effort levels
|
|
31
31
|
zeta can reconfigure and extend itself — ask any questions you have!
|
|
32
|
-
Lint/type errors piling up? `zetacode cleanse` (or /cleanse right here) hunts project diagnostics and fixes them with parallel subagents — {key:escape} cancels
|
|
33
|
-
[NO-TERN] Psst, zeta has a natural habitat 🐦 [tern](https://scl.so/tern)
|
|
32
|
+
Lint/type errors piling up? `zetacode cleanse` (or /cleanse right here) hunts project diagnostics and fixes them with parallel subagents — {key:escape} cancels
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The word being typed in the composer. Misspelling marks hold off on it until
|
|
3
|
+
* the caret leaves it (a space or punctuation, a caret move, losing focus), the
|
|
4
|
+
* way Slack and Chromium mark a typo only once its word is done; marks on other
|
|
5
|
+
* words stay while typing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** A word of one logical line, as UTF-16 columns `[start, end)`. */
|
|
9
|
+
export interface TypingWord {
|
|
10
|
+
line: number;
|
|
11
|
+
start: number;
|
|
12
|
+
end: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Letters, marks and digits, with apostrophes joining contractions. */
|
|
16
|
+
const WORD_CHAR = /[\p{L}\p{M}\p{N}'\u2019]/u;
|
|
17
|
+
|
|
18
|
+
/** The run of word characters around column `col` of `line` (`undefined` between non-word characters). */
|
|
19
|
+
function wordAround(line: string, col: number): { start: number; end: number } | undefined {
|
|
20
|
+
let start = col;
|
|
21
|
+
while (start > 0 && WORD_CHAR.test(line.charAt(start - 1))) start--;
|
|
22
|
+
let end = col;
|
|
23
|
+
while (end < line.length && WORD_CHAR.test(line.charAt(end))) end++;
|
|
24
|
+
return start < end ? { start, end } : undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Follows an editor's text revision and caret to know which word is being typed. */
|
|
28
|
+
export class TypingWordTracker {
|
|
29
|
+
#revision: number | undefined;
|
|
30
|
+
#word: TypingWord | undefined;
|
|
31
|
+
|
|
32
|
+
/** The word being typed, if any. */
|
|
33
|
+
get word(): TypingWord | undefined {
|
|
34
|
+
return this.#word;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Catch up with the editor: an edit that leaves the caret in (or right after)
|
|
39
|
+
* a word starts typing it; a caret move out of that word, or losing focus,
|
|
40
|
+
* ends it. The first call only records the revision, so a restored draft is
|
|
41
|
+
* not held. Returns whether the held word changed.
|
|
42
|
+
*/
|
|
43
|
+
update(revision: number, lines: readonly string[], line: number, col: number, focused: boolean): boolean {
|
|
44
|
+
const before = this.#word;
|
|
45
|
+
const edited = this.#revision !== undefined && this.#revision !== revision;
|
|
46
|
+
this.#revision = revision;
|
|
47
|
+
if (!focused) {
|
|
48
|
+
this.#word = undefined;
|
|
49
|
+
} else if (edited) {
|
|
50
|
+
const span = wordAround(lines[line] ?? "", col);
|
|
51
|
+
this.#word = span && { line, start: span.start, end: span.end };
|
|
52
|
+
} else if (before && (before.line !== line || col < before.start || col > before.end)) {
|
|
53
|
+
this.#word = undefined;
|
|
54
|
+
}
|
|
55
|
+
return (
|
|
56
|
+
before?.line !== this.#word?.line || before?.start !== this.#word?.start || before?.end !== this.#word?.end
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/prompt/welcome.ts
CHANGED
|
@@ -15,27 +15,21 @@ import type { Component } from "../tui";
|
|
|
15
15
|
import { padding, replaceTabs, visibleWidth, wrapTextWithAnsi } from "../utils";
|
|
16
16
|
import tipsText from "./tips.txt" with { type: "text" };
|
|
17
17
|
|
|
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*/;
|
|
21
|
-
|
|
22
18
|
/** Trailing marker that flags a tip as a "what's new" callout. Stripped before
|
|
23
19
|
* wrapping (with any preceding whitespace) and replaced by {@link NEW_TAG_TEXT}
|
|
24
20
|
* painted as a shimmering rainbow. Non-global so `.test` stays stateless. */
|
|
25
21
|
const NEW_TIP_MARKER = /\s*\[NEW\]\s*$/;
|
|
26
22
|
|
|
27
|
-
/** A tips.txt line. The
|
|
28
|
-
* pool selection and the
|
|
23
|
+
/** A tips.txt line. The `[NEW]` marker stays in the source line — it drives
|
|
24
|
+
* pool selection and the weighting — while the user-visible text is
|
|
29
25
|
* resolved from the host catalogue under `tipN`, N = the 1-based line number. */
|
|
30
26
|
interface TipLine {
|
|
31
27
|
/** 1-based tips.txt line number; the catalogue key is `tip${line}`. */
|
|
32
28
|
readonly line: number;
|
|
33
|
-
/** Raw line,
|
|
29
|
+
/** Raw line, marker intact — what the weighted picker selects from. */
|
|
34
30
|
readonly raw: string;
|
|
35
|
-
/** English body with
|
|
31
|
+
/** English body with the `[NEW]` marker stripped: the `tuiText` fallback. */
|
|
36
32
|
readonly english: string;
|
|
37
|
-
/** Pitches Tern; excluded from the TSP pool. */
|
|
38
|
-
readonly noTern: boolean;
|
|
39
33
|
/** "What's new" callout; carries the heavier selection weight. */
|
|
40
34
|
readonly isNew: boolean;
|
|
41
35
|
}
|
|
@@ -49,19 +43,47 @@ const TIP_LINES: readonly TipLine[] = tipsText.split("\n").flatMap((line, index)
|
|
|
49
43
|
{
|
|
50
44
|
line: index + 1,
|
|
51
45
|
raw,
|
|
52
|
-
english: raw.replace(
|
|
53
|
-
noTern: NO_TERN_TIP_MARKER.test(raw),
|
|
46
|
+
english: raw.replace(NEW_TIP_MARKER, ""),
|
|
54
47
|
isNew: NEW_TIP_MARKER.test(raw),
|
|
55
48
|
},
|
|
56
49
|
];
|
|
57
50
|
});
|
|
58
51
|
|
|
59
|
-
/**
|
|
60
|
-
*
|
|
52
|
+
/** Every tip's raw line, so {@link pickWeightedTip} can still read the
|
|
53
|
+
* trailing {@link NEW_TIP_MARKER} for its selection weight. */
|
|
61
54
|
const TIPS: readonly string[] = TIP_LINES.map(tip => tip.raw);
|
|
62
55
|
|
|
63
|
-
/**
|
|
64
|
-
|
|
56
|
+
/** Catalogue keys (`tip${line}`) of every tips.txt line, in file order. The
|
|
57
|
+
* host needs them to gate a tip by the product feature it advertises. */
|
|
58
|
+
export const TIP_CATALOGUE_KEYS: readonly string[] = TIP_LINES.map(tip => `tip${tip.line}`);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Host-installable tip gate: receives the tip's catalogue key and its raw
|
|
62
|
+
* tips.txt line; returning false removes it from the splash pool.
|
|
63
|
+
*/
|
|
64
|
+
export type TuiTipFilter = (key: string, raw: string) => boolean;
|
|
65
|
+
|
|
66
|
+
let tipFilter: TuiTipFilter | undefined;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Install the host's tip gate (pass `undefined` to reset). Consulted for every
|
|
70
|
+
* candidate on every pick, so no roll can surface a rejected tip — used when a
|
|
71
|
+
* feature a tip advertises is switched off by configuration.
|
|
72
|
+
*/
|
|
73
|
+
export function setTuiTipFilter(filter: TuiTipFilter | undefined): void {
|
|
74
|
+
tipFilter = filter;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The pool {@link WelcomeComponent} picks from, with the host gate applied. */
|
|
78
|
+
function tipPool(): readonly string[] {
|
|
79
|
+
// Captured so the narrowing survives into the filter callback.
|
|
80
|
+
const filter = tipFilter;
|
|
81
|
+
if (!filter) return TIPS;
|
|
82
|
+
return TIPS.filter(tip => {
|
|
83
|
+
const entry = TIP_LINES.find(candidate => candidate.raw === tip);
|
|
84
|
+
return entry === undefined || filter(`tip${entry.line}`, tip);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
65
87
|
|
|
66
88
|
/** Visible text rendered in place of {@link NEW_TIP_MARKER}. */
|
|
67
89
|
const NEW_TAG_TEXT = "NEW!";
|
|
@@ -99,7 +121,7 @@ interface ResolvedTip {
|
|
|
99
121
|
readonly isNew: boolean;
|
|
100
122
|
}
|
|
101
123
|
|
|
102
|
-
/** Localise a picked tip. `raw` is a {@link TIPS}
|
|
124
|
+
/** Localise a picked tip. `raw` is a {@link TIPS} member (or the
|
|
103
125
|
* nerdfont nudge); the catalogue is consulted live, so `/language` applies. */
|
|
104
126
|
function resolveTip(raw: string): ResolvedTip {
|
|
105
127
|
if (raw === NERDFONT_TIP) return { text: tuiText("welcomeNerdFontJoke", NERDFONT_TIP), isNew: false };
|
|
@@ -165,7 +187,7 @@ function expandTipKeys(tip: string): string {
|
|
|
165
187
|
});
|
|
166
188
|
}
|
|
167
189
|
|
|
168
|
-
/** Markdown-style links in tips.txt: `[
|
|
190
|
+
/** Markdown-style links in tips.txt: `[docs](https://example.com)` shows the label, linked to the URL. */
|
|
169
191
|
const TIP_LINK = /\[([^\]]+)\]\(([^\s)]+)\)/g;
|
|
170
192
|
|
|
171
193
|
/** Tip links as underlined OSC 8 labels, or `label (url)` where the terminal cannot link. */
|
|
@@ -238,10 +260,11 @@ export function renderWelcomeTip(tip: string, width: number, phase = 0, isNew =
|
|
|
238
260
|
}
|
|
239
261
|
|
|
240
262
|
/**
|
|
241
|
-
* The session's welcome banner. In a terminal: the gradient
|
|
242
|
-
* `
|
|
263
|
+
* The session's welcome banner. In a terminal: the gradient mark beside the
|
|
264
|
+
* `Zeta` wordmark, with the version under the wordmark and the `Linxira-Zeta`
|
|
265
|
+
* identity subline centered below the pair (the mark alone when the lockup does
|
|
243
266
|
* not fit) and the tip of the session (dropped below {@link TIP_MIN_COLUMNS}
|
|
244
|
-
* columns). Natively: a card with the same
|
|
267
|
+
* columns). Natively: a card with the same mark, wordmark, version and tip
|
|
245
268
|
* ({@link WelcomeComponent.describe}).
|
|
246
269
|
*/
|
|
247
270
|
export class WelcomeComponent implements Component {
|
|
@@ -249,10 +272,8 @@ export class WelcomeComponent implements Component {
|
|
|
249
272
|
#animTimer: Timer | null = null;
|
|
250
273
|
#requestRender: (() => void) | null = null;
|
|
251
274
|
// Tip randomness is latched once so the tip is stable across renders, but
|
|
252
|
-
// the nerdfont-nag gate re-reads the live preset
|
|
253
|
-
//
|
|
254
|
-
// preset before settings resolve the real one, and an optimistic Tern start
|
|
255
|
-
// falls back to the classic renderer when `hello` never confirms.
|
|
275
|
+
// the nerdfont-nag gate re-reads the live preset: the startup prepaint can
|
|
276
|
+
// run under the default "unicode" preset before settings resolve the real one.
|
|
256
277
|
#nagRoll: number | undefined;
|
|
257
278
|
#tipRoll: number | undefined;
|
|
258
279
|
// Render cache: the welcome box is the first transcript-area component, so
|
|
@@ -272,7 +293,7 @@ export class WelcomeComponent implements Component {
|
|
|
272
293
|
if (theme.getSymbolPreset() === "unicode" && this.#nagRoll < 0.1) {
|
|
273
294
|
return NERDFONT_TIP;
|
|
274
295
|
}
|
|
275
|
-
return pickWeightedTip(
|
|
296
|
+
return pickWeightedTip(tipPool(), this.#tipRoll) || undefined;
|
|
276
297
|
}
|
|
277
298
|
|
|
278
299
|
invalidate(): void {
|
|
@@ -284,7 +305,8 @@ export class WelcomeComponent implements Component {
|
|
|
284
305
|
/**
|
|
285
306
|
* A `card` (`zeta.welcome`) mirroring the terminal banner: the lockup
|
|
286
307
|
* (`zeta.welcome.lockup`: the terminal's builtin `zeta` mark, which it animates,
|
|
287
|
-
* beside the wordmark with the version
|
|
308
|
+
* beside the `Zeta` wordmark with the version and the `Linxira-Zeta`
|
|
309
|
+
* subline under it) and the tip of the session.
|
|
288
310
|
* Roles carry the look (gradient logo, type scale); a "[NEW]" tip
|
|
289
311
|
* carries a terminal-clocked shimmering tag.
|
|
290
312
|
*/
|
|
@@ -313,8 +335,9 @@ export class WelcomeComponent implements Component {
|
|
|
313
335
|
keyed(
|
|
314
336
|
col(
|
|
315
337
|
[
|
|
316
|
-
art([span("
|
|
338
|
+
art([span("Zeta", "strong")], "zeta.welcome.wordmark"),
|
|
317
339
|
art([span(`v${this.version}`, "dim mono")], "zeta.welcome.version"),
|
|
340
|
+
art([span("Linxira-Zeta", "dim")], "zeta.welcome.subline"),
|
|
318
341
|
],
|
|
319
342
|
{ role: "zeta.welcome.mark" },
|
|
320
343
|
),
|
|
@@ -466,22 +489,39 @@ export class WelcomeComponent implements Component {
|
|
|
466
489
|
}
|
|
467
490
|
}
|
|
468
491
|
|
|
469
|
-
|
|
470
|
-
|
|
492
|
+
// The three arrays below are GENERATED by the standard FIGlet tool, never
|
|
493
|
+
// hand-drawn: `bun scripts/brand/generate-logo-art.ts` rewrites them from the
|
|
494
|
+
// FIGlet fonts vendored under `scripts/brand/fonts/` (mark + wordmark in the
|
|
495
|
+
// standard "ANSI Shadow" font, the `Linxira-Zeta` subline in "mini"), and
|
|
496
|
+
// `bun scripts/brand/brand-check.ts` fails on any hand-edited glyph. Standard
|
|
497
|
+
// FIGlet fonts carry no Greek block, so the mark is the Latin `Z` — the same
|
|
498
|
+
// glyph as `Ζ` (see `scripts/brand/logo-art.ts`).
|
|
499
|
+
|
|
500
|
+
/** Brand mark: FIGlet `ANSI Shadow` render of `Z` (generated). */
|
|
501
|
+
export const ZETA_LOGO = ["███████╗", "╚══███╔╝", " ███╔╝ ", " ███╔╝ ", "███████╗", "╚══════╝"];
|
|
502
|
+
|
|
503
|
+
/** Primary product name: FIGlet `ANSI Shadow` render of `Zeta` (generated). */
|
|
504
|
+
export const ZETA_WORDMARK = [
|
|
505
|
+
"███████╗███████╗████████╗ █████╗ ",
|
|
506
|
+
"╚══███╔╝██╔════╝╚══██╔══╝██╔══██╗",
|
|
507
|
+
" ███╔╝ █████╗ ██║ ███████║",
|
|
508
|
+
" ███╔╝ ██╔══╝ ██║ ██╔══██║",
|
|
509
|
+
"███████╗███████╗ ██║ ██║ ██║",
|
|
510
|
+
"╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝",
|
|
511
|
+
];
|
|
471
512
|
|
|
472
|
-
/**
|
|
473
|
-
const
|
|
513
|
+
/** Secondary identity line: FIGlet `mini` render of `Linxira-Zeta` (generated). */
|
|
514
|
+
export const ZETA_SUBLINE = [
|
|
515
|
+
" __ ",
|
|
516
|
+
" | o ._ o ._ _. __ / _ _|_ _. ",
|
|
517
|
+
" |_ | | | >< | | (_| /_ (/_ |_ (_| ",
|
|
518
|
+
];
|
|
474
519
|
|
|
475
|
-
/**
|
|
476
|
-
|
|
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 = ["███▀ ▄ ", " ██ ███▄ ███ ▄██▄", "▄▄██ █▄▄ █▄ █▄▄█"];
|
|
520
|
+
/** Columns of {@link ZETA_LOGO}. */
|
|
521
|
+
const LOGO_WIDTH = Math.max(...ZETA_LOGO.map(row => visibleWidth(row)));
|
|
482
522
|
|
|
483
|
-
/** Columns of {@link
|
|
484
|
-
const WORDMARK_WIDTH = Math.max(...
|
|
523
|
+
/** Columns of {@link ZETA_WORDMARK}. */
|
|
524
|
+
const WORDMARK_WIDTH = Math.max(...ZETA_WORDMARK.map(row => visibleWidth(row)));
|
|
485
525
|
|
|
486
526
|
/** Columns between the logo and the wordmark. */
|
|
487
527
|
const LOCKUP_GAP = 4;
|
|
@@ -492,10 +532,22 @@ const TIP_MEASURE = 72;
|
|
|
492
532
|
/** Narrowest terminal that still shows the tip; below it the banner is the logo alone. */
|
|
493
533
|
const TIP_MIN_COLUMNS = 50;
|
|
494
534
|
|
|
495
|
-
/**
|
|
535
|
+
/**
|
|
536
|
+
* The lockup block: the mark beside the wordmark, `version` (styled) under the
|
|
537
|
+
* wordmark, and the `Linxira-Zeta` subline centered under the pair.
|
|
538
|
+
*/
|
|
496
539
|
function lockup(logo: readonly string[], version: string): string[] {
|
|
497
|
-
const beside = [
|
|
498
|
-
|
|
540
|
+
const beside = [...ZETA_WORDMARK.map(row => theme.bold(theme.fg("text", row))), version];
|
|
541
|
+
const rows = Math.max(logo.length, beside.length);
|
|
542
|
+
const art = Array.from({ length: rows }, (_, index) => {
|
|
543
|
+
const mark = logo[index] ?? "";
|
|
544
|
+
return `${mark}${padding(LOGO_WIDTH - visibleWidth(mark) + LOCKUP_GAP)}${beside[index] ?? ""}`;
|
|
545
|
+
});
|
|
546
|
+
const blockWidth = Math.max(...art.map(line => visibleWidth(line)));
|
|
547
|
+
for (const row of ZETA_SUBLINE) {
|
|
548
|
+
art.push(padding(Math.max(0, Math.floor((blockWidth - visibleWidth(row)) / 2))) + theme.fg("dim", row));
|
|
549
|
+
}
|
|
550
|
+
return art;
|
|
499
551
|
}
|
|
500
552
|
|
|
501
553
|
/**
|
|
@@ -508,7 +560,7 @@ function centerBlock(lines: readonly string[], width: number): string[] {
|
|
|
508
560
|
return lines.map(line => indent + line);
|
|
509
561
|
}
|
|
510
562
|
|
|
511
|
-
/** The
|
|
563
|
+
/** The generated brand mark as accent lines; `shimmer` declares the terminal-clocked shine sweep. */
|
|
512
564
|
export function logoNode(lines: readonly string[], shimmer: boolean): NativeNode {
|
|
513
565
|
return col(
|
|
514
566
|
lines.map(line =>
|
package/src/render/code-cell.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { code, md, span } from "../native/describe";
|
|
|
8
8
|
import type { NativeNode } from "../native/node";
|
|
9
9
|
import { getMarkdownTheme, highlightCode, type Theme } from "../theme/theme";
|
|
10
10
|
import { formatDuration, formatExpandHint, formatMoreItems, formatStatusIcon, replaceTabs } from "./render-utils";
|
|
11
|
+
import { wrapPrefixedLine } from "./hanging-wrap";
|
|
11
12
|
import {
|
|
12
13
|
describeOutputBlock,
|
|
13
14
|
type NativeOutputBlockSection,
|
|
@@ -167,6 +168,7 @@ export function renderCodeCell(options: CodeCellOptions, theme: Theme): string[]
|
|
|
167
168
|
}
|
|
168
169
|
}
|
|
169
170
|
|
|
171
|
+
const numberedCodeLines: string[] = [];
|
|
170
172
|
if (lineNumberWidth > 0 && visibleLineNumbers) {
|
|
171
173
|
for (let i = 0; i < codeLines.length; i++) {
|
|
172
174
|
const lineNum = visibleLineNumbers[i];
|
|
@@ -174,10 +176,14 @@ export function renderCodeCell(options: CodeCellOptions, theme: Theme): string[]
|
|
|
174
176
|
lineNum !== null && lineNum !== undefined
|
|
175
177
|
? String(lineNum).padStart(lineNumberWidth, " ")
|
|
176
178
|
: " ".repeat(lineNumberWidth);
|
|
177
|
-
|
|
179
|
+
const prefix = theme.fg("dim", `${gutter} `);
|
|
180
|
+
numberedCodeLines.push(
|
|
181
|
+
...wrapPrefixedLine(prefix, " ".repeat(lineNumberWidth + 1), codeLines[i]!, outputBlockContentWidth(width)),
|
|
182
|
+
);
|
|
178
183
|
}
|
|
179
184
|
}
|
|
180
185
|
|
|
186
|
+
const displayCodeLines = lineNumberWidth > 0 ? numberedCodeLines : codeLines;
|
|
181
187
|
if (hiddenCodeLines > 0) {
|
|
182
188
|
const hint = formatExpandHint(theme, expanded, hiddenCodeLines > 0);
|
|
183
189
|
const gutterPad = lineNumberWidth > 0 ? " ".repeat(lineNumberWidth + 1) : "";
|
|
@@ -185,16 +191,16 @@ export function renderCodeCell(options: CodeCellOptions, theme: Theme): string[]
|
|
|
185
191
|
// Earlier rows scrolled above the live tail window — mark them on top so
|
|
186
192
|
// the newest streamed line stays pinned to the bottom of the box.
|
|
187
193
|
const earlier = `… ${hiddenCodeLines} earlier line${hiddenCodeLines === 1 ? "" : "s"}${hint ? ` ${hint}` : ""}`;
|
|
188
|
-
|
|
194
|
+
displayCodeLines.unshift(theme.fg("dim", gutterPad + earlier));
|
|
189
195
|
} else {
|
|
190
196
|
const moreLine = `${formatMoreItems(hiddenCodeLines, "line")}${hint ? ` ${hint}` : ""}`;
|
|
191
|
-
|
|
197
|
+
displayCodeLines.push(theme.fg("dim", gutterPad + moreLine));
|
|
192
198
|
}
|
|
193
199
|
}
|
|
194
200
|
|
|
195
201
|
const outputLines = renderCellOutput(output, expanded, outputMaxLines, theme);
|
|
196
202
|
|
|
197
|
-
const sections: Array<{ label?: string; lines: readonly string[] }> = [{ lines:
|
|
203
|
+
const sections: Array<{ label?: string; lines: readonly string[] }> = [{ lines: displayCodeLines }];
|
|
198
204
|
if (outputLines.length > 0) {
|
|
199
205
|
sections.push({ label: theme.fg("toolTitle", "Output"), lines: outputLines });
|
|
200
206
|
}
|
|
@@ -252,7 +258,14 @@ export function describeCodeCell(options: Omit<CodeCellOptions, "width">, theme:
|
|
|
252
258
|
const sections: NativeOutputBlockSection[] = [
|
|
253
259
|
{
|
|
254
260
|
key: "code",
|
|
255
|
-
body: [
|
|
261
|
+
body: [
|
|
262
|
+
code(options.code ?? "", {
|
|
263
|
+
lang: options.language,
|
|
264
|
+
start,
|
|
265
|
+
numbers: start !== undefined || undefined,
|
|
266
|
+
wrap: true,
|
|
267
|
+
}),
|
|
268
|
+
],
|
|
256
269
|
},
|
|
257
270
|
];
|
|
258
271
|
const output = describeCellOutput(options.output, expanded, outputMaxLines);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Theme } from "../theme/theme";
|
|
2
|
+
/** Wrap content under a distinct first-row prefix, preserving its column on continuation rows. */
|
|
3
|
+
export declare function wrapPrefixedLine(prefix: string, continuationPrefix: string, content: string, width: number): string[];
|
|
4
|
+
/** Preserve a tree connector's ancestor guides while hanging a long node beneath its label. */
|
|
5
|
+
export declare function wrapTreeLine(line: string, width: number, theme: Theme): string[] | undefined;
|
|
6
|
+
/** Hang a tree row beneath its connector, or a connector-less detail row beneath its guide rails. */
|
|
7
|
+
export declare function wrapGuidedLine(line: string, width: number, theme: Theme): string[];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { visibleWidth, wrapTextWithAnsi } from "../utils";
|
|
2
|
+
import type { Theme } from "../theme/theme";
|
|
3
|
+
|
|
4
|
+
/** Wrap content under a distinct first-row prefix, preserving its column on continuation rows. */
|
|
5
|
+
export function wrapPrefixedLine(prefix: string, continuationPrefix: string, content: string, width: number): string[] {
|
|
6
|
+
const prefixWidth = visibleWidth(prefix);
|
|
7
|
+
const continuationWidth = visibleWidth(continuationPrefix);
|
|
8
|
+
if (width <= Math.max(prefixWidth, continuationWidth)) return wrapTextWithAnsi(prefix + content, width);
|
|
9
|
+
const rows = wrapTextWithAnsi(content, width - Math.max(prefixWidth, continuationWidth));
|
|
10
|
+
return rows.map((row, index) => `${index === 0 ? prefix : continuationPrefix}${row}`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Preserve a tree connector's ancestor guides while hanging a long node beneath its label. */
|
|
14
|
+
export function wrapTreeLine(line: string, width: number, theme: Theme): string[] | undefined {
|
|
15
|
+
for (const connector of [theme.tree.branch, theme.tree.last]) {
|
|
16
|
+
const marker = theme.fg("dim", connector);
|
|
17
|
+
const index = line.indexOf(marker);
|
|
18
|
+
if (index < 0) continue;
|
|
19
|
+
const before = line.slice(0, index);
|
|
20
|
+
if (!/^ *$/.test(Bun.stripANSI(before).replaceAll(theme.tree.vertical, ""))) continue;
|
|
21
|
+
const end = index + marker.length + 1;
|
|
22
|
+
if (line[end - 1] !== " ") continue;
|
|
23
|
+
const connectorWidth = visibleWidth(connector);
|
|
24
|
+
const guide =
|
|
25
|
+
connector === theme.tree.last
|
|
26
|
+
? " ".repeat(connectorWidth)
|
|
27
|
+
: `${theme.fg("dim", theme.tree.vertical)}${" ".repeat(Math.max(0, connectorWidth - visibleWidth(theme.tree.vertical)))}`;
|
|
28
|
+
return wrapPrefixedLine(line.slice(0, end), `${before}${guide} `, line.slice(end), width);
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const SGR = /\x1b\[[0-9;]*m/y;
|
|
34
|
+
|
|
35
|
+
/** End of the leading run of spaces, `vertical` guides and SGR escapes that a tree detail row hangs beneath. */
|
|
36
|
+
function guideRunEnd(line: string, vertical: string): number {
|
|
37
|
+
let index = 0;
|
|
38
|
+
while (index < line.length) {
|
|
39
|
+
if (line[index] === " ") {
|
|
40
|
+
index++;
|
|
41
|
+
} else if (vertical && line.startsWith(vertical, index)) {
|
|
42
|
+
index += vertical.length;
|
|
43
|
+
} else {
|
|
44
|
+
SGR.lastIndex = index;
|
|
45
|
+
if (!SGR.test(line)) break;
|
|
46
|
+
index = SGR.lastIndex;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return index;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Hang a tree row beneath its connector, or a connector-less detail row beneath its guide rails. */
|
|
53
|
+
export function wrapGuidedLine(line: string, width: number, theme: Theme): string[] {
|
|
54
|
+
const tree = wrapTreeLine(line, width, theme);
|
|
55
|
+
if (tree) return tree;
|
|
56
|
+
const prefix = line.slice(0, guideRunEnd(line, theme.tree.vertical));
|
|
57
|
+
return wrapPrefixedLine(prefix, prefix, line.slice(prefix.length), width);
|
|
58
|
+
}
|
|
@@ -70,6 +70,8 @@ export declare function urlHyperlinkAlways(url: string, displayText: string): st
|
|
|
70
70
|
* Non-HTTP(S) or unparsable URLs yield a plain span.
|
|
71
71
|
*/
|
|
72
72
|
export declare function urlLinkSpan(target: string, displayText: string, s?: string): TspSpan;
|
|
73
|
+
/** Whether native link spans and nodes may carry an `href`: everything but an explicit `off` policy. */
|
|
74
|
+
export declare function isNativeLinkEnabled(): boolean;
|
|
73
75
|
/**
|
|
74
76
|
* A native link span to a filesystem path as a plain `file://` URI (the
|
|
75
77
|
* terminal opens it through the system; the location stays in the visible
|
package/src/render/hyperlink.ts
CHANGED
|
@@ -196,6 +196,11 @@ export function urlLinkSpan(target: string, displayText: string, s = "link"): Ts
|
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
/** Whether native link spans and nodes may carry an `href`: everything but an explicit `off` policy. */
|
|
200
|
+
export function isNativeLinkEnabled(): boolean {
|
|
201
|
+
return hyperlinkMode !== "off";
|
|
202
|
+
}
|
|
203
|
+
|
|
199
204
|
/**
|
|
200
205
|
* A native link span to a filesystem path as a plain `file://` URI (the
|
|
201
206
|
* terminal opens it through the system; the location stays in the visible
|
|
@@ -105,14 +105,22 @@ export declare function cappedHeadLines(lines: readonly string[], max: number):
|
|
|
105
105
|
};
|
|
106
106
|
/** Get the first nonblank preview lines, trimmed and width-capped. */
|
|
107
107
|
export declare function getPreviewLines(text: string, maxLines: number, maxLineLen: number, ellipsis?: Ellipsis): string[];
|
|
108
|
+
/** Collapse a possibly multi-line string into one line; whitespace runs (tabs included) become one space. */
|
|
109
|
+
export declare function flattenLine(text: string): string;
|
|
108
110
|
/**
|
|
109
111
|
* Collapse a possibly multi-line string into a single line, then truncate it to
|
|
110
112
|
* `maxWidth` display cells. {@link truncateToWidth} alone caps width but
|
|
111
113
|
* newlines are zero-width, so multi-line content (markdown briefs, tool args,
|
|
112
114
|
* provider errors) would otherwise spill a single status row across several
|
|
113
|
-
* visual lines.
|
|
115
|
+
* visual lines.
|
|
114
116
|
*/
|
|
115
117
|
export declare function previewLine(text: string, maxWidth: number, ellipsis?: Ellipsis): string;
|
|
118
|
+
/**
|
|
119
|
+
* Clip a result row to `maxWidth` only while collapsed. Expanded rows stay whole
|
|
120
|
+
* so the frame wraps them: the expand key ({@link formatExpandHint}) must reveal
|
|
121
|
+
* text, not re-clip it.
|
|
122
|
+
*/
|
|
123
|
+
export declare function clipCollapsed(text: string, maxWidth: number, expanded: boolean | undefined): string;
|
|
116
124
|
/**
|
|
117
125
|
* Extract domain from URL, stripping www. prefix.
|
|
118
126
|
*/
|
|
@@ -233,15 +233,29 @@ export function getPreviewLines(text: string, maxLines: number, maxLineLen: numb
|
|
|
233
233
|
return lines.slice(0, maxLines).map(l => truncateToWidth(l.trim(), maxLineLen, ellipsis));
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
/** Collapse a possibly multi-line string into one line; whitespace runs (tabs included) become one space. */
|
|
237
|
+
export function flattenLine(text: string): string {
|
|
238
|
+
return text.replace(/\s+/g, " ").trim();
|
|
239
|
+
}
|
|
240
|
+
|
|
236
241
|
/**
|
|
237
242
|
* Collapse a possibly multi-line string into a single line, then truncate it to
|
|
238
243
|
* `maxWidth` display cells. {@link truncateToWidth} alone caps width but
|
|
239
244
|
* newlines are zero-width, so multi-line content (markdown briefs, tool args,
|
|
240
245
|
* provider errors) would otherwise spill a single status row across several
|
|
241
|
-
* visual lines.
|
|
246
|
+
* visual lines.
|
|
242
247
|
*/
|
|
243
248
|
export function previewLine(text: string, maxWidth: number, ellipsis?: Ellipsis): string {
|
|
244
|
-
return truncateToWidth(text
|
|
249
|
+
return truncateToWidth(flattenLine(text), maxWidth, ellipsis);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Clip a result row to `maxWidth` only while collapsed. Expanded rows stay whole
|
|
254
|
+
* so the frame wraps them: the expand key ({@link formatExpandHint}) must reveal
|
|
255
|
+
* text, not re-clip it.
|
|
256
|
+
*/
|
|
257
|
+
export function clipCollapsed(text: string, maxWidth: number, expanded: boolean | undefined): string {
|
|
258
|
+
return expanded ? text : truncateToWidth(text, maxWidth);
|
|
245
259
|
}
|
|
246
260
|
|
|
247
261
|
// =============================================================================
|
package/src/render/utils.d.ts
CHANGED
|
@@ -32,7 +32,7 @@ export interface RenderCache {
|
|
|
32
32
|
lines: string[];
|
|
33
33
|
}
|
|
34
34
|
/** Build indentation and continuing branches for ancestor levels. */
|
|
35
|
-
export declare function buildTreePrefix(ancestors: boolean[], theme: Theme): string;
|
|
35
|
+
export declare function buildTreePrefix(ancestors: readonly boolean[], theme: Theme): string;
|
|
36
36
|
/** Return the branch glyph for a final or continuing tree item. */
|
|
37
37
|
export declare function getTreeBranch(isLast: boolean, theme: Theme): string;
|
|
38
38
|
/** Return the continuation prefix for subsequent lines of a tree item. */
|
package/src/render/utils.ts
CHANGED
|
@@ -76,7 +76,7 @@ export interface RenderCache {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/** Build indentation and continuing branches for ancestor levels. */
|
|
79
|
-
export function buildTreePrefix(ancestors: boolean[], theme: Theme): string {
|
|
79
|
+
export function buildTreePrefix(ancestors: readonly boolean[], theme: Theme): string {
|
|
80
80
|
return ancestors.map(hasNext => (hasNext ? `${theme.tree.vertical} ` : " ")).join("");
|
|
81
81
|
}
|
|
82
82
|
|
|
@@ -25,7 +25,6 @@ export class WidthAwareText implements Component {
|
|
|
25
25
|
#cachedContentWidth = -1;
|
|
26
26
|
#cachedText: string | undefined;
|
|
27
27
|
#ignoreTight = false;
|
|
28
|
-
#nativeCols = -1;
|
|
29
28
|
#native: NativeNode | undefined;
|
|
30
29
|
|
|
31
30
|
constructor(format: (contentWidth: number) => string, paddingX = 1, paddingY = 1) {
|
|
@@ -71,10 +70,12 @@ export class WidthAwareText implements Component {
|
|
|
71
70
|
* (the only width a describer knows) and again on `reformat()`.
|
|
72
71
|
*/
|
|
73
72
|
describe(cx: DescribeContext): NativeNode {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
// Native describe emits no padding, so the formatter gets the full
|
|
74
|
+
// column budget — the same width a native Text.describe() would use.
|
|
75
|
+
const contentWidth = Math.max(1, cx.cols);
|
|
76
|
+
if (this.#cachedText === undefined || contentWidth !== this.#cachedContentWidth) {
|
|
77
|
+
this.#cachedContentWidth = contentWidth;
|
|
78
|
+
this.#cachedText = this.#format(contentWidth);
|
|
78
79
|
this.#inner.setText(this.#cachedText);
|
|
79
80
|
}
|
|
80
81
|
this.#native ??= col([this.#inner]);
|