@linxiraos/pi-tui 1.2.1 → 1.3.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 +245 -224
- 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/status-notice.d.ts +2 -0
- 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 +23 -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/web-search-types.d.ts +1 -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/charts/table-data.ts +17 -6
- 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/status-notice.ts +5 -0
- 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 +482 -12
- 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 +4 -1
- package/src/prompt/composer.ts +200 -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 +79 -11
- 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 +300 -62
- 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 +190 -63
- 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-types.d.ts +1 -0
- package/src/tools/web-search-types.ts +1 -0
- 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 +160 -43
- 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
package/src/apps/git/git-tui.ts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* scrollbar, right sidebar (file management + commit form while dirty, HEAD
|
|
8
8
|
* commit details with author avatar when clean). Submitting an empty commit
|
|
9
9
|
* form generates an llm-git-compatible message; submitting populated fields
|
|
10
|
-
* commits them. A footer shows key hints
|
|
10
|
+
* commits them. A footer shows key hints; the staging and discarding ones are
|
|
11
|
+
* omitted while the tree is clean, where they cannot fire.
|
|
11
12
|
*
|
|
12
13
|
* `tab` moves focus between the diff and the sidebar; both panes take
|
|
13
14
|
* arrows/PgUp/PgDn, vim motions (`j`/`k`/`h`/`l`/`g`/`G`), and mouse
|
|
@@ -22,7 +23,8 @@
|
|
|
22
23
|
* refreshes. In the sidebar tree `←`/`→` collapse/expand directories,
|
|
23
24
|
* `enter` opens the selected file in the diff pane, `space` stages or
|
|
24
25
|
* unstages the selected row, and `delete` discards it — on a directory,
|
|
25
|
-
* every file underneath it.
|
|
26
|
+
* every file underneath it. All of the staging and discarding keys are inert
|
|
27
|
+
* on a clean tree, which shows the HEAD commit instead of a change list.
|
|
26
28
|
*/
|
|
27
29
|
|
|
28
30
|
import type { TspSpan } from "@linxiraos/pi-wire";
|
|
@@ -32,7 +34,7 @@ import { Stack } from "../../components/layout/stack";
|
|
|
32
34
|
import { matchesKey } from "../../keys";
|
|
33
35
|
import { ProcessTerminal } from "../../terminal";
|
|
34
36
|
import { routeSgrMouseInput } from "../../mouse";
|
|
35
|
-
import { col, compact, keyed, node, row, span, text } from "../../native/describe";
|
|
37
|
+
import { col, compact, keyed, node, row, span, stableKey, text } from "../../native/describe";
|
|
36
38
|
import { Memo } from "../../native/memo";
|
|
37
39
|
import type { DescribeContext, NativeNode, NativeUiEvent } from "../../native/node";
|
|
38
40
|
import { actionButton } from "../../native/overlay";
|
|
@@ -42,6 +44,7 @@ import { theme, warmHighlighter } from "../../theme/theme";
|
|
|
42
44
|
import type { AvatarSource } from "./avatar";
|
|
43
45
|
import { pill, softPill, tintChip } from "./colors";
|
|
44
46
|
import {
|
|
47
|
+
assetTitle,
|
|
45
48
|
buildDiffDocument,
|
|
46
49
|
type DiffDocument,
|
|
47
50
|
buildLineSelectionPatch,
|
|
@@ -53,7 +56,7 @@ import {
|
|
|
53
56
|
} from "./diff-pane";
|
|
54
57
|
import { GitHelpSheet } from "./help";
|
|
55
58
|
import { Sidebar, type SidebarAction, type GitCommitMessage } from "./sidebar";
|
|
56
|
-
import type { AssetFileContents, ChangedFile, FileContents, GitTuiModel } from "./state";
|
|
59
|
+
import type { AssetFileContents, ChangedFile, FileAssetSide, FileContents, GitTuiModel, MediaRef } from "./state";
|
|
57
60
|
|
|
58
61
|
/** AI staging counts displayed after filtering the working tree. */
|
|
59
62
|
export interface AiStageOutcome {
|
|
@@ -88,6 +91,28 @@ const STATUS_TTL_MS = 6_000;
|
|
|
88
91
|
|
|
89
92
|
type Focus = "diff" | "sidebar";
|
|
90
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Footer key hints. A clean tree renders the HEAD commit view, where stage
|
|
96
|
+
* and discard cannot fire: `#patchTargetFor` returns null and
|
|
97
|
+
* `#discardCurrentFile` returns early for `area: "commit"` rows, and the
|
|
98
|
+
* sidebar only builds stage/discard actions for the unstaged/staged
|
|
99
|
+
* sections. `commit` needs the commit form (absent on a clean tree) and
|
|
100
|
+
* `select` feeds the line-level stage/discard actions (no-ops on commit
|
|
101
|
+
* rows), so the commit view drops those too and says what it is showing.
|
|
102
|
+
*/
|
|
103
|
+
export function gitFooterHints(clean: boolean, focus: Focus): string {
|
|
104
|
+
const edit = clean
|
|
105
|
+
? ""
|
|
106
|
+
: focus === "diff"
|
|
107
|
+
? ` · ${formatKeyHints(["s", "u"])} stage · ${formatKeyHints(["x", "delete"])} discard`
|
|
108
|
+
: ` · ${formatKeyHint("space")} stage · ${formatKeyHint("delete")} discard`;
|
|
109
|
+
const commit = clean ? "" : ` · ${formatKeyHint("c")} commit`;
|
|
110
|
+
const select = clean || focus !== "diff" ? "" : ` · ${formatKeyHints(["shift+up", "shift+down"])} select`;
|
|
111
|
+
return focus === "diff"
|
|
112
|
+
? `${formatKeyHints(["alt+down", "alt+up"])} hunk · ${formatKeyHints(["]", "["])} file${select}${edit} · ${formatKeyHint("v")} view${commit} · ${formatKeyHint("?")} keys · ${formatKeyHint("q")} quit`
|
|
113
|
+
: `${formatKeyHints(["up", "down"])} move · ${formatKeyHints(["left", "right"])} fold${edit} · ${formatKeyHint("enter")} open · ${formatKeyHints(["alt+down", "alt+up"])} hunk${commit} · ${formatKeyHint("t")} tree · ${formatKeyHint("?")} keys · ${formatKeyHint("q")} quit`;
|
|
114
|
+
}
|
|
115
|
+
|
|
91
116
|
/** Tone of a status message: its colour in the header and its span token natively. */
|
|
92
117
|
type StatusTone = "success" | "warning" | "error" | "accent" | "dim";
|
|
93
118
|
|
|
@@ -792,14 +817,7 @@ class GitTuiComponent implements Component {
|
|
|
792
817
|
|
|
793
818
|
// The empty middle carries the key hints (or a fresh status message).
|
|
794
819
|
const status = this.#visibleStatus() ? theme.fg(this.#statusTone, this.#status) : "";
|
|
795
|
-
const middle =
|
|
796
|
-
status ||
|
|
797
|
-
theme.fg(
|
|
798
|
-
"dim",
|
|
799
|
-
this.#focus === "diff"
|
|
800
|
-
? `${formatKeyHints(["alt+down", "alt+up"])} hunk · ${formatKeyHints(["]", "["])} file · ${formatKeyHints(["shift+up", "shift+down"])} select · ${formatKeyHints(["s", "u"])} stage · ${formatKeyHints(["x", "delete"])} discard · ${formatKeyHint("v")} view · ${formatKeyHint("c")} commit · ${formatKeyHint("?")} keys · ${formatKeyHint("q")} quit`
|
|
801
|
-
: `${formatKeyHints(["up", "down"])} move · ${formatKeyHints(["left", "right"])} fold · ${formatKeyHint("space")} stage · ${formatKeyHint("delete")} discard · ${formatKeyHint("enter")} open · ${formatKeyHints(["alt+down", "alt+up"])} hunk · ${formatKeyHint("c")} commit · ${formatKeyHint("t")} tree · ${formatKeyHint("?")} keys · ${formatKeyHint("q")} quit`,
|
|
802
|
-
);
|
|
820
|
+
const middle = status || theme.fg("dim", gitFooterHints(this.#model.clean, this.#focus));
|
|
803
821
|
const free = width - row.width - right.width - 1;
|
|
804
822
|
const middleText = free > visibleWidth(middle) + 4 ? middle : truncateToWidth(middle, Math.max(0, free - 4));
|
|
805
823
|
const leftPad = Math.max(1, Math.floor((free - visibleWidth(middleText)) / 2));
|
|
@@ -929,8 +947,9 @@ class GitTuiComponent implements Component {
|
|
|
929
947
|
this.#contents,
|
|
930
948
|
this.#currentFile,
|
|
931
949
|
this.#pane.emptyMessage,
|
|
950
|
+
cx.supports("media"),
|
|
932
951
|
],
|
|
933
|
-
() => this.#describeBody(),
|
|
952
|
+
() => this.#describeBody(cx),
|
|
934
953
|
);
|
|
935
954
|
const side = this.#sidebar.describe(cx);
|
|
936
955
|
return this.#nativeRoot.get([bar, body, side, narrow, status, this.#status, this.#statusTone], () => {
|
|
@@ -1086,7 +1105,7 @@ class GitTuiComponent implements Component {
|
|
|
1086
1105
|
}
|
|
1087
1106
|
|
|
1088
1107
|
/** The diff for the view mode, or the loading, empty and media states. */
|
|
1089
|
-
#describeBody(): NativeNode {
|
|
1108
|
+
#describeBody(cx: DescribeContext): NativeNode {
|
|
1090
1109
|
const doc = this.#pane.doc;
|
|
1091
1110
|
const file = this.#currentFile;
|
|
1092
1111
|
if (doc && this.#pane.state === "ready") {
|
|
@@ -1104,6 +1123,18 @@ class GitTuiComponent implements Component {
|
|
|
1104
1123
|
}
|
|
1105
1124
|
const contents = this.#contents;
|
|
1106
1125
|
if (this.#pane.state === "asset" && file && contents?.kind === "asset") {
|
|
1126
|
+
if (cx.supports("media")) {
|
|
1127
|
+
return keyed(
|
|
1128
|
+
row(
|
|
1129
|
+
[
|
|
1130
|
+
mediaColumn("before", contents.old, contents.oldRef),
|
|
1131
|
+
mediaColumn("after", contents.new, contents.newRef),
|
|
1132
|
+
],
|
|
1133
|
+
{ gap: "sm", align: "stretch", grow: 1, role: "zeta.app.git.compare" },
|
|
1134
|
+
),
|
|
1135
|
+
"compare",
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1107
1138
|
return keyed(
|
|
1108
1139
|
col(
|
|
1109
1140
|
[
|
|
@@ -1190,6 +1221,41 @@ function unifiedDiff(doc: DiffDocument, whole: boolean): string {
|
|
|
1190
1221
|
return out.join("\n");
|
|
1191
1222
|
}
|
|
1192
1223
|
|
|
1224
|
+
/** `media` nodes of the before/after compare share this link, so they pan and zoom together. */
|
|
1225
|
+
const MEDIA_COMPARE_LINK = "git-compare";
|
|
1226
|
+
|
|
1227
|
+
/**
|
|
1228
|
+
* One column of the native media compare: its heading (`Before · PNG`) over a
|
|
1229
|
+
* `media` node Tern loads from `ref`, keyed by path so another file remounts,
|
|
1230
|
+
* or a muted line when the side has no file.
|
|
1231
|
+
*/
|
|
1232
|
+
function mediaColumn(side: "before" | "after", asset: FileAssetSide, ref: MediaRef | undefined): NativeNode {
|
|
1233
|
+
const head = keyed(
|
|
1234
|
+
text([span(assetTitle(side === "before" ? "Before" : "After", asset), "strong")], {
|
|
1235
|
+
truncate: "end",
|
|
1236
|
+
role: "zeta.app.git.media-head",
|
|
1237
|
+
}),
|
|
1238
|
+
"head",
|
|
1239
|
+
);
|
|
1240
|
+
const body = ref
|
|
1241
|
+
? node(
|
|
1242
|
+
"media",
|
|
1243
|
+
{ ...ref, link: MEDIA_COMPARE_LINK, grow: 1, role: "zeta.app.git.media" },
|
|
1244
|
+
undefined,
|
|
1245
|
+
`media-${stableKey(ref.path)}`,
|
|
1246
|
+
)
|
|
1247
|
+
: keyed(
|
|
1248
|
+
text(
|
|
1249
|
+
asset.kind === "empty"
|
|
1250
|
+
? [span(side === "before" ? "Did not exist" : "Deleted", "muted")]
|
|
1251
|
+
: assetText(asset).map(fact => span(fact.t, "muted")),
|
|
1252
|
+
{ role: "zeta.app.git.media-none" },
|
|
1253
|
+
),
|
|
1254
|
+
"none",
|
|
1255
|
+
);
|
|
1256
|
+
return keyed(col([head, body], { gap: "xs", grow: 1, basis: 0, role: "zeta.app.git.media-side" }), side);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1193
1259
|
/** One side of a media/binary change as a short fact line. */
|
|
1194
1260
|
function assetText(asset: AssetFileContents["old"]): TspSpan[] {
|
|
1195
1261
|
const size = (bytes: number | undefined): string =>
|
package/src/apps/git/sidebar.ts
CHANGED
|
@@ -1681,6 +1681,9 @@ export class Sidebar {
|
|
|
1681
1681
|
text: ` ${theme.fg("dim", tuiText("gitParentLabel", "parent:"))} ${theme.fg("accent", head.parents.map(sha => sha.slice(0, 8)).join(" "))}`,
|
|
1682
1682
|
});
|
|
1683
1683
|
}
|
|
1684
|
+
// This view only appears when there is nothing to stage or discard, so say so
|
|
1685
|
+
// rather than letting the file list read as pending work.
|
|
1686
|
+
rows.push({ text: theme.fg("success", ` Working tree clean — showing committed ${head.shortSha}`) });
|
|
1684
1687
|
rows.push({ text: theme.fg("borderMuted", "─".repeat(Math.max(0, width))) });
|
|
1685
1688
|
if (!head.filesLoaded) {
|
|
1686
1689
|
rows.push({ text: theme.fg("dim", tuiText("gitLoadingChangedFiles", " Loading changed files…")) });
|
|
@@ -1689,8 +1692,14 @@ export class Sidebar {
|
|
|
1689
1692
|
|
|
1690
1693
|
const additions = head.files.reduce((sum, file) => sum + (file.additions ?? 0), 0);
|
|
1691
1694
|
const deletions = head.files.reduce((sum, file) => sum + (file.deletions ?? 0), 0);
|
|
1695
|
+
// Upstream's redesigned row ("N files in <sha>"); the count and the sha
|
|
1696
|
+
// fragment both come from the catalogue so a zh session localizes them.
|
|
1697
|
+
const count =
|
|
1698
|
+
head.files.length === 1
|
|
1699
|
+
? tuiTextFmt("gitModifiedCountOneFmt", "%s file", head.files.length)
|
|
1700
|
+
: tuiTextFmt("gitModifiedCountManyFmt", "%s files", head.files.length);
|
|
1692
1701
|
rows.push({
|
|
1693
|
-
text: ` ${theme.bold(
|
|
1702
|
+
text: ` ${theme.bold(count)} ${theme.fg("dim", tuiTextFmt("gitInShaFmt", `in ${head.shortSha}`, head.shortSha))} ${theme.fg("success", `+${additions}`)} ${theme.fg("error", `−${deletions}`)}`,
|
|
1694
1703
|
});
|
|
1695
1704
|
rows.push(this.#viewToggleRow(width));
|
|
1696
1705
|
rows.push(...this.#entryRows(head.files, "commit"));
|
package/src/apps/git/state.ts
CHANGED
|
@@ -66,11 +66,23 @@ export interface TextFileContents {
|
|
|
66
66
|
readonly streamResult: DiffStreamResult;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/** Where one side of a change lives, so a host viewer (Tern's TSP `media` node) can load it by path. */
|
|
70
|
+
export interface MediaRef {
|
|
71
|
+
/** Absolute path of the file (repository root joined with its repository path). */
|
|
72
|
+
readonly path: string;
|
|
73
|
+
/** Git revision holding this side (`HEAD`, a sha, `:0` for the index); absent for the worktree file. */
|
|
74
|
+
readonly rev?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
69
77
|
/** Non-text sides rendered as previews or explicit placeholders. */
|
|
70
78
|
export interface AssetFileContents {
|
|
71
79
|
readonly kind: "asset";
|
|
72
80
|
readonly old: FileAssetSide;
|
|
73
81
|
readonly new: FileAssetSide;
|
|
82
|
+
/** Source of {@link old}; absent when that side is empty. */
|
|
83
|
+
readonly oldRef?: MediaRef;
|
|
84
|
+
/** Source of {@link new}; absent when that side is empty. */
|
|
85
|
+
readonly newRef?: MediaRef;
|
|
74
86
|
}
|
|
75
87
|
|
|
76
88
|
/** Loaded file content selected for text diffing or asset preview. */
|
package/src/autocomplete.d.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export declare function findLeadingSlashCommandStart(text: string): number | null;
|
|
8
8
|
export declare function findTrailingSlashCommandStart(text: string): number | null;
|
|
9
|
+
/** Extract the live whitespace-delimited or double-quoted `@` file token ending at the cursor. */
|
|
10
|
+
export declare function extractAtFilePrefix(text: string): string | null;
|
|
9
11
|
/**
|
|
10
12
|
* Whether an autocomplete value represents a directory: trailing slash or
|
|
11
13
|
* backslash, optionally followed by a closing quote for quoted paths.
|
package/src/autocomplete.ts
CHANGED
|
@@ -14,6 +14,11 @@ const PATH_DELIMITERS = new Set([" ", "\t", '"', "'", "="]);
|
|
|
14
14
|
*/
|
|
15
15
|
const AT_PARTIAL_DELAY_MS = 150;
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Without a deadline the native walk follows symlinks at unbounded depth and never returns.
|
|
19
|
+
*/
|
|
20
|
+
const AT_FUZZY_TIMEOUT_MS = 1_500;
|
|
21
|
+
|
|
17
22
|
function buildAutocompleteFuzzyDiscoveryProfile(
|
|
18
23
|
query: string,
|
|
19
24
|
basePath: string,
|
|
@@ -25,6 +30,7 @@ function buildAutocompleteFuzzyDiscoveryProfile(
|
|
|
25
30
|
hidden: boolean;
|
|
26
31
|
gitignore: boolean;
|
|
27
32
|
cache: boolean;
|
|
33
|
+
timeoutMs: number;
|
|
28
34
|
signal?: AbortSignal;
|
|
29
35
|
} {
|
|
30
36
|
return {
|
|
@@ -34,6 +40,7 @@ function buildAutocompleteFuzzyDiscoveryProfile(
|
|
|
34
40
|
hidden: true,
|
|
35
41
|
gitignore: true,
|
|
36
42
|
cache: true,
|
|
43
|
+
timeoutMs: AT_FUZZY_TIMEOUT_MS,
|
|
37
44
|
...(signal ? { signal } : {}),
|
|
38
45
|
};
|
|
39
46
|
}
|
|
@@ -106,6 +113,18 @@ function extractQuotedPrefix(text: string): string | null {
|
|
|
106
113
|
return text.slice(quoteStart);
|
|
107
114
|
}
|
|
108
115
|
|
|
116
|
+
/** Extract the live whitespace-delimited or double-quoted `@` file token ending at the cursor. */
|
|
117
|
+
export function extractAtFilePrefix(text: string): string | null {
|
|
118
|
+
const quotedPrefix = extractQuotedPrefix(text);
|
|
119
|
+
if (quotedPrefix?.startsWith('@"')) {
|
|
120
|
+
return quotedPrefix;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const lastDelimiterIndex = findLastDelimiter(text);
|
|
124
|
+
const tokenStart = lastDelimiterIndex === -1 ? 0 : lastDelimiterIndex + 1;
|
|
125
|
+
return text[tokenStart] === "@" ? text.slice(tokenStart) : null;
|
|
126
|
+
}
|
|
127
|
+
|
|
109
128
|
function parsePathPrefix(prefix: string): { rawPrefix: string; isAtPrefix: boolean; isQuotedPrefix: boolean } {
|
|
110
129
|
if (prefix.startsWith('@"')) {
|
|
111
130
|
return { rawPrefix: prefix.slice(2), isAtPrefix: true, isQuotedPrefix: true };
|
|
@@ -719,7 +738,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
719
738
|
}
|
|
720
739
|
|
|
721
740
|
// Check for @ file reference (fuzzy search) - must be after a delimiter or at start
|
|
722
|
-
const atPrefix =
|
|
741
|
+
const atPrefix = extractAtFilePrefix(textBeforeCursor);
|
|
723
742
|
if (atPrefix) {
|
|
724
743
|
const { rawPrefix, isQuotedPrefix } = parsePathPrefix(atPrefix);
|
|
725
744
|
// Recursive fuzzy walks rooted outside the project (e.g. `@../`,
|
|
@@ -862,7 +881,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
862
881
|
|
|
863
882
|
// Check if we're completing a file attachment (prefix starts with "@")
|
|
864
883
|
if (prefix.startsWith("@")) {
|
|
865
|
-
const liveAtPrefix =
|
|
884
|
+
const liveAtPrefix = extractAtFilePrefix(textBeforeCursor);
|
|
866
885
|
if (liveAtPrefix) {
|
|
867
886
|
beforePrefix = currentLine.slice(0, cursorCol - liveAtPrefix.length);
|
|
868
887
|
}
|
|
@@ -896,23 +915,6 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
896
915
|
};
|
|
897
916
|
}
|
|
898
917
|
|
|
899
|
-
// Extract @ prefix for fuzzy file suggestions
|
|
900
|
-
#extractAtPrefix(text: string): string | null {
|
|
901
|
-
const quotedPrefix = extractQuotedPrefix(text);
|
|
902
|
-
if (quotedPrefix?.startsWith('@"')) {
|
|
903
|
-
return quotedPrefix;
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
const lastDelimiterIndex = findLastDelimiter(text);
|
|
907
|
-
const tokenStart = lastDelimiterIndex === -1 ? 0 : lastDelimiterIndex + 1;
|
|
908
|
-
|
|
909
|
-
if (text[tokenStart] === "@") {
|
|
910
|
-
return text.slice(tokenStart);
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
return null;
|
|
914
|
-
}
|
|
915
|
-
|
|
916
918
|
// Extract a path-like prefix from the text before cursor
|
|
917
919
|
#extractPathPrefix(text: string, forceExtract: boolean = false): string | null {
|
|
918
920
|
const quotedPrefix = extractQuotedPrefix(text);
|
package/src/bracketed-paste.ts
CHANGED
|
@@ -68,6 +68,7 @@ const DEFAULT_BYTE_LIMIT = 64 * 1024 * 1024;
|
|
|
68
68
|
*/
|
|
69
69
|
export class BracketedPasteHandler {
|
|
70
70
|
#buffer = "";
|
|
71
|
+
#bufferBytes = 0;
|
|
71
72
|
#active = false;
|
|
72
73
|
readonly #byteLimit: number;
|
|
73
74
|
|
|
@@ -88,7 +89,10 @@ export class BracketedPasteHandler {
|
|
|
88
89
|
const start = data.indexOf(PASTE_START);
|
|
89
90
|
const end = data.indexOf(PASTE_END);
|
|
90
91
|
if (start !== -1 && (end === -1 || start < end)) {
|
|
91
|
-
if (!this.#active)
|
|
92
|
+
if (!this.#active) {
|
|
93
|
+
this.#buffer = "";
|
|
94
|
+
this.#bufferBytes = 0;
|
|
95
|
+
}
|
|
92
96
|
this.#active = true;
|
|
93
97
|
// Only strip nested starts before this paste's first end. Later starts
|
|
94
98
|
// belong to the next paste in `remaining`.
|
|
@@ -100,6 +104,7 @@ export class BracketedPasteHandler {
|
|
|
100
104
|
|
|
101
105
|
if (!this.#active) return { handled: false };
|
|
102
106
|
|
|
107
|
+
this.#bufferBytes += Buffer.byteLength(data, "utf8");
|
|
103
108
|
this.#buffer += data;
|
|
104
109
|
|
|
105
110
|
const endIndex = this.#buffer.indexOf(PASTE_END);
|
|
@@ -108,6 +113,7 @@ export class BracketedPasteHandler {
|
|
|
108
113
|
const remaining = this.#buffer.substring(endIndex + PASTE_END.length);
|
|
109
114
|
|
|
110
115
|
this.#buffer = "";
|
|
116
|
+
this.#bufferBytes = 0;
|
|
111
117
|
this.#active = false;
|
|
112
118
|
|
|
113
119
|
return { handled: true, pasteContent, remaining };
|
|
@@ -118,9 +124,10 @@ export class BracketedPasteHandler {
|
|
|
118
124
|
// neither lost nor held forever, and reset paste mode so subsequent
|
|
119
125
|
// input recovers. See `StdinBuffer#abortPaste` for the sibling recovery
|
|
120
126
|
// semantics inside `StdinBuffer`.
|
|
121
|
-
if (this.#
|
|
127
|
+
if (this.#bufferBytes > this.#byteLimit) {
|
|
122
128
|
const pasteContent = this.#buffer;
|
|
123
129
|
this.#buffer = "";
|
|
130
|
+
this.#bufferBytes = 0;
|
|
124
131
|
this.#active = false;
|
|
125
132
|
return { handled: true, pasteContent, remaining: "" };
|
|
126
133
|
}
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* Draws a {@link ChartSpec} as a standalone SVG document colored only with
|
|
3
3
|
* figure tokens — `var(--fg)`, `var(--muted)`, `var(--border)`,
|
|
4
4
|
* `var(--accent)`, `var(--surface)`, `var(--c1)`…`var(--c6)` — on a
|
|
5
|
-
* transparent background — the token vocabulary of assistant ```svg figures
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* transparent background — the token vocabulary of assistant ```svg figures:
|
|
6
|
+
* the host resolves them against the live theme (`prepareSvg`) for the
|
|
7
|
+
* rasterized terminal image, and a TSP terminal draws the chart as an ```svg
|
|
8
|
+
* figure in the palette it shows. The root carries {@link chartAlt} as its
|
|
9
|
+
* `aria-label`.
|
|
8
10
|
*
|
|
9
11
|
* Styled after Apple's charts and Tern's UI: a UI sans (Geist where Tern
|
|
10
12
|
* provides it), muted labels, hairline gridlines that recede behind the
|
|
@@ -39,16 +41,8 @@
|
|
|
39
41
|
*/
|
|
40
42
|
import { type ChartSpec } from "./chart-plan";
|
|
41
43
|
import { type Dimension } from "./table-data";
|
|
42
|
-
/**
|
|
43
|
-
export
|
|
44
|
-
readonly svg: string;
|
|
45
|
-
readonly width: number;
|
|
46
|
-
readonly height: number;
|
|
47
|
-
}
|
|
48
|
-
/** The SVG document for `spec`. */
|
|
49
|
-
export declare function renderChartSvg(spec: ChartSpec): ChartSvg;
|
|
50
|
-
/** One-line description of the chart, for screen readers and terminals that show the alt text. */
|
|
51
|
-
export declare function chartAlt(spec: ChartSpec): string;
|
|
44
|
+
/** The SVG document for `spec`, labeled for screen readers with {@link chartAlt}. */
|
|
45
|
+
export declare function renderChartSvg(spec: ChartSpec): string;
|
|
52
46
|
/**
|
|
53
47
|
* A value in base units as an axis label: durations and sizes in the unit,
|
|
54
48
|
* counts compacted under the suffix (`12k`, `3.4M`), that `magnitude` reads
|
package/src/charts/chart-svg.ts
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* Draws a {@link ChartSpec} as a standalone SVG document colored only with
|
|
3
3
|
* figure tokens — `var(--fg)`, `var(--muted)`, `var(--border)`,
|
|
4
4
|
* `var(--accent)`, `var(--surface)`, `var(--c1)`…`var(--c6)` — on a
|
|
5
|
-
* transparent background — the token vocabulary of assistant ```svg figures
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* transparent background — the token vocabulary of assistant ```svg figures:
|
|
6
|
+
* the host resolves them against the live theme (`prepareSvg`) for the
|
|
7
|
+
* rasterized terminal image, and a TSP terminal draws the chart as an ```svg
|
|
8
|
+
* figure in the palette it shows. The root carries {@link chartAlt} as its
|
|
9
|
+
* `aria-label`.
|
|
8
10
|
*
|
|
9
11
|
* Styled after Apple's charts and Tern's UI: a UI sans (Geist where Tern
|
|
10
12
|
* provides it), muted labels, hairline gridlines that recede behind the
|
|
@@ -236,15 +238,8 @@ interface Tier {
|
|
|
236
238
|
readonly high: number;
|
|
237
239
|
}
|
|
238
240
|
|
|
239
|
-
/**
|
|
240
|
-
export
|
|
241
|
-
readonly svg: string;
|
|
242
|
-
readonly width: number;
|
|
243
|
-
readonly height: number;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/** The SVG document for `spec`. */
|
|
247
|
-
export function renderChartSvg(spec: ChartSpec): ChartSvg {
|
|
241
|
+
/** The SVG document for `spec`, labeled for screen readers with {@link chartAlt}. */
|
|
242
|
+
export function renderChartSvg(spec: ChartSpec): string {
|
|
248
243
|
const layer = drawChart(spec);
|
|
249
244
|
const header = drawHeader(spec, layer);
|
|
250
245
|
const head = matrixHead(spec);
|
|
@@ -268,8 +263,8 @@ export function renderChartSvg(spec: ChartSpec): ChartSvg {
|
|
|
268
263
|
height += 20;
|
|
269
264
|
}
|
|
270
265
|
height = Math.ceil(height + PAD);
|
|
271
|
-
|
|
272
|
-
`<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" height="${height}" viewBox="0 0 ${WIDTH} ${height}" font-family="${FONT}" font-size="${LABEL_SIZE}">`,
|
|
266
|
+
return [
|
|
267
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" height="${height}" viewBox="0 0 ${WIDTH} ${height}" role="img" aria-label="${escapeXml(chartAlt(spec))}" font-family="${FONT}" font-size="${LABEL_SIZE}">`,
|
|
273
268
|
layer.body.includes(`url(#${INK_MASK})`) ? INK_DEFS : "",
|
|
274
269
|
`<g transform="translate(0 ${PAD})">${header.body}</g>`,
|
|
275
270
|
head.body && `<g transform="translate(0 ${num(PAD + header.height)})">${head.body}</g>`,
|
|
@@ -278,11 +273,10 @@ export function renderChartSvg(spec: ChartSpec): ChartSvg {
|
|
|
278
273
|
caption,
|
|
279
274
|
"</svg>",
|
|
280
275
|
].join("");
|
|
281
|
-
return { svg, width: WIDTH, height };
|
|
282
276
|
}
|
|
283
277
|
|
|
284
|
-
/** One-line description of the chart, for screen readers
|
|
285
|
-
|
|
278
|
+
/** One-line description of the chart, for screen readers. */
|
|
279
|
+
function chartAlt(spec: ChartSpec): string {
|
|
286
280
|
const joint =
|
|
287
281
|
spec.kind === "stacked" ? " + " : spec.kind === "slope" ? " → " : spec.kind === "butterfly" ? " vs " : ", ";
|
|
288
282
|
const names = spec.series.map(entry => entry.name).join(joint);
|
|
@@ -4432,6 +4426,13 @@ function token(name: string): string {
|
|
|
4432
4426
|
return `var(--${name})`;
|
|
4433
4427
|
}
|
|
4434
4428
|
|
|
4429
|
+
/** `raw` safe as XML text or a double-quoted attribute value. */
|
|
4430
|
+
function escapeXml(raw: string): string {
|
|
4431
|
+
return raw.replace(/[&<>"]/g, char =>
|
|
4432
|
+
char === "&" ? "&" : char === "<" ? "<" : char === ">" ? ">" : """,
|
|
4433
|
+
);
|
|
4434
|
+
}
|
|
4435
|
+
|
|
4435
4436
|
function num(value: number): string {
|
|
4436
4437
|
return Number(value.toFixed(1)).toString();
|
|
4437
4438
|
}
|
|
@@ -4469,10 +4470,6 @@ function text(x: number, y: number, content: string | readonly TextRun[], style:
|
|
|
4469
4470
|
style.weight ? `font-weight="${style.weight}"` : "",
|
|
4470
4471
|
style.italic ? `font-style="italic"` : "",
|
|
4471
4472
|
].filter(Boolean);
|
|
4472
|
-
const escape = (raw: string) =>
|
|
4473
|
-
raw.replace(/[&<>"]/g, char =>
|
|
4474
|
-
char === "&" ? "&" : char === "<" ? "<" : char === ">" ? ">" : """,
|
|
4475
|
-
);
|
|
4476
4473
|
const body = runs
|
|
4477
4474
|
.filter(run => run.content)
|
|
4478
4475
|
.map(run => {
|
|
@@ -4480,7 +4477,7 @@ function text(x: number, y: number, content: string | readonly TextRun[], style:
|
|
|
4480
4477
|
run.fill ? ` fill="${token(run.fill)}"` : "",
|
|
4481
4478
|
run.weight ? ` font-weight="${run.weight}"` : "",
|
|
4482
4479
|
].join("");
|
|
4483
|
-
return own ? `<tspan${own}>${
|
|
4480
|
+
return own ? `<tspan${own}>${escapeXml(run.content)}</tspan>` : escapeXml(run.content);
|
|
4484
4481
|
})
|
|
4485
4482
|
.join("");
|
|
4486
4483
|
return `<text ${attrs.join(" ")}>${body}</text>`;
|
package/src/charts/table-data.ts
CHANGED
|
@@ -148,7 +148,17 @@ export interface TableAnalysis {
|
|
|
148
148
|
readonly composition?: Composition;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
|
|
151
|
+
/**
|
|
152
|
+
* A whole number as written: bare, or grouped in thousands by commas (`1,988`)
|
|
153
|
+
* or by spaces as many locales write it (`1 988`, also U+00A0 and U+202F).
|
|
154
|
+
* Space groups take exactly three digits, so `12 5` and `128 2560` stay apart.
|
|
155
|
+
*/
|
|
156
|
+
const INTEGER = String.raw`\d{1,3}(?:,\d{3})+|\d{1,3}(?:[ \u00A0\u202F]\d{3})+(?!\d)|\d+`;
|
|
157
|
+
const NUMBER = String.raw`(?:${INTEGER})(?:\.\d+)?(?:[eE][+-]?\d+)?|\.\d+`;
|
|
158
|
+
/** A cell holding one whole number and nothing else (`38`, `1,980`). */
|
|
159
|
+
const WHOLE = new RegExp(String.raw`^(?:${INTEGER})$`);
|
|
160
|
+
/** The first number of a figure, its whole part and its decimals apart. */
|
|
161
|
+
const WRITTEN = new RegExp(String.raw`(${INTEGER})(?:\.(\d+))?`);
|
|
152
162
|
const CELL = new RegExp(
|
|
153
163
|
String.raw`^(?<approx>~|≈|<=|>=|<|>|≤|≥)?\s*(?<sign>[+\-−±])?\s*(?<cur>[$€£¥])?\s*(?<num>${NUMBER})(?<plus>\+(?=\s*[A-Za-zµμ%]))?\s*(?<unit>%|[A-Za-zµμ×]+(?:\/[A-Za-z]+)?|\/s)?(?<rest>.*)$`,
|
|
154
164
|
);
|
|
@@ -517,8 +527,9 @@ export function parseCell(markdown: string): Cell {
|
|
|
517
527
|
return { ...cell, fit: "prose" };
|
|
518
528
|
}
|
|
519
529
|
|
|
530
|
+
/** The value of a number {@link NUMBER} matched, its comma or space group separators removed. */
|
|
520
531
|
function toNumber(written: string): number {
|
|
521
|
-
return Number(written.
|
|
532
|
+
return Number(written.replace(/[,\s]/g, ""));
|
|
522
533
|
}
|
|
523
534
|
|
|
524
535
|
/** A number of a list or sum: its value as written, its unit (known or a noun) and how it was marked. */
|
|
@@ -632,7 +643,7 @@ function listCell(text: string, base: CellBase): NumberCell | undefined {
|
|
|
632
643
|
// A spaced pair of bare numbers that is no score (`25.0 / 27.8`, `12.6 / 9.4`: the second is no whole, or
|
|
633
644
|
// the first exceeds it) lists two values of the column's quantity; `27 / 30` stays a score.
|
|
634
645
|
const [first, second] = items;
|
|
635
|
-
const whole =
|
|
646
|
+
const whole = WHOLE.test(text.slice(text.indexOf("/") + 1, end).trim());
|
|
636
647
|
const score = whole && first!.value >= 0 && first!.value <= second!.value;
|
|
637
648
|
const pair = items.length === 2 && !marked.some(Boolean) && separators[0] !== "/" && !score;
|
|
638
649
|
if (!pair && (!marked.at(-1) || !(marked.every(Boolean) || carried))) return undefined;
|
|
@@ -876,9 +887,9 @@ const WHOLE_HEADER = /\b(?:total|all|sum|overall)\b/i;
|
|
|
876
887
|
*/
|
|
877
888
|
export function writtenSlack(cell: NumberCell): number {
|
|
878
889
|
if (cell.denominator !== undefined && cell.dim === "percent") return 0;
|
|
879
|
-
const match =
|
|
890
|
+
const match = WRITTEN.exec(cell.figure);
|
|
880
891
|
if (!match) return 0;
|
|
881
|
-
const written =
|
|
892
|
+
const written = toNumber(`${match[1]!}${match[2] ? `.${match[2]}` : ""}`);
|
|
882
893
|
const factor = written ? Math.abs(cell.value) / written : 1;
|
|
883
894
|
const decimals = match[2]?.length ?? 0;
|
|
884
895
|
if (cell.dim === "count" && decimals === 0 && factor === 1 && !cell.approx) return 0;
|
|
@@ -1097,7 +1108,7 @@ const MIN_LISTS = 2;
|
|
|
1097
1108
|
/** A spaced slash between list items. */
|
|
1098
1109
|
const LIST_SLASH = /\s+\/\s+/;
|
|
1099
1110
|
/** A list item that is one number, as written (`12`, `~1,400`, `3.5 ms`, `40%`). */
|
|
1100
|
-
const LISTED_NUMBER =
|
|
1111
|
+
const LISTED_NUMBER = new RegExp(String.raw`^[~≈]?[$€£¥]?(?:${INTEGER})(?:\.\d+)?\s*(?:%|[A-Za-zµμ]+)?$`);
|
|
1101
1112
|
|
|
1102
1113
|
/**
|
|
1103
1114
|
* Rows folding several rows into one (`response anthropic / chat / responses /
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Component } from "../tui";
|
|
2
2
|
import type { AdvisorMessageDetails } from "./messages";
|
|
3
|
-
import {
|
|
3
|
+
import type { Theme } from "../theme";
|
|
4
4
|
import { type NativeUiEvent } from "../native/node";
|
|
5
5
|
/** The advisor card: shared by the controlled expansion getter and native toggles. */
|
|
6
6
|
export interface AdvisorMessageCard extends Component {
|
|
@@ -14,6 +14,9 @@ export interface AdvisorMessageCard extends Component {
|
|
|
14
14
|
* output (whose `thinkingText` color equals `toolOutput` in most themes):
|
|
15
15
|
* a bold `customMessageLabel` header tag (skill-card convention), a heavy
|
|
16
16
|
* rail tinted per-note severity, and the note body on the default text color.
|
|
17
|
+
* Natively it is a callout tinted by its worst note: that severity's icon and
|
|
18
|
+
* the tally head it, the advisor's name ends the head, and with several notes
|
|
19
|
+
* each leads with its own severity icon.
|
|
17
20
|
*
|
|
18
21
|
* `getExpanded` owns expansion; a native toggle overrides it for this card
|
|
19
22
|
* until the next transcript-wide change.
|