@linxiraos/pi-tui 1.2.0 → 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 -120
- 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 +22 -26
- 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 +35 -43
- 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/agent-hub.ts +2 -2
- 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/plugin-settings.ts +8 -2
- 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 -6
- package/src/prompt/composer.ts +196 -96
- 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 +256 -423
- 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 +137 -60
- 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
|
@@ -10,10 +10,13 @@ import {
|
|
|
10
10
|
} from "@linxiraos/pi-utils/marked";
|
|
11
11
|
import {
|
|
12
12
|
type MathBlockOpener,
|
|
13
|
+
type MathOpener,
|
|
13
14
|
mathBlockCloserIndex,
|
|
14
15
|
mathBlockInContext,
|
|
15
16
|
mathBlockMayCloseAt,
|
|
16
17
|
mathBlockOpenerAt,
|
|
18
|
+
mathOpenerAt,
|
|
19
|
+
mathSpanAt,
|
|
17
20
|
mathSpanInContext,
|
|
18
21
|
mathStartIndex,
|
|
19
22
|
} from "@linxiraos/pi-utils/math-delimiters";
|
|
@@ -52,6 +55,361 @@ function normalizeOsc8Terminators(text: string): string {
|
|
|
52
55
|
return text.replace(OSC8_ST_PREFIX_REGEX, "$1\x07");
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
/**
|
|
59
|
+
* `text` as the streaming renderer lexes it: OSC 8 terminators unified and
|
|
60
|
+
* tabs expanded. {@link Markdown.getLastRenderStableText} is a prefix of this
|
|
61
|
+
* form, not of the raw source, so append-only publishers that mix settled
|
|
62
|
+
* prefixes with whole blocks must compare both in this form. Rendering it is
|
|
63
|
+
* byte-identical to rendering `text`.
|
|
64
|
+
*/
|
|
65
|
+
export function streamingMarkdownSource(text: string): string {
|
|
66
|
+
if (!text.includes("\t") && !text.includes("\x1b")) return text;
|
|
67
|
+
return replaceTabs(normalizeOsc8Terminators(text));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// A leading-pipe table line: optional indent / blockquote markers, then `|`.
|
|
71
|
+
const TABLE_LINE_AT = /[ \t>]*\|/y;
|
|
72
|
+
// A line holding an unescaped `|`: a GFM table line needs no outer pipes.
|
|
73
|
+
const PIPE_IN_LINE_AT = /[^\n]*?(?<!\\)\|/y;
|
|
74
|
+
// A whole GFM delimiter row (`| --- | :-: |`, `--- | ---`). Without a leading pipe it needs an inner one; a lone
|
|
75
|
+
// `---` is a rule or setext underline.
|
|
76
|
+
const DELIMITER_ROW_AT =
|
|
77
|
+
/[ \t>]*(?:\|[ \t]*:?-+:?[ \t]*(?:\|[ \t]*:?-+:?[ \t]*)*\|?|[ \t]*:?-+:?[ \t]*(?:\|[ \t]*:?-+:?[ \t]*)+\|?)[ \t]*\r?(?=\n|$)/y;
|
|
78
|
+
const UNESCAPED_PIPE = /(?<!\\)\|/g;
|
|
79
|
+
const UNESCAPED_PIPE_END = /(?<!\\)\|$/;
|
|
80
|
+
// A fence line (opener or closer) after any blockquote markers: indentation, marker, info string.
|
|
81
|
+
const FENCE_LINE = /^(?:[ \t]*>)*([ \t]*)(`{3,}|~{3,})([^\n]*)$/gm;
|
|
82
|
+
// A list item's marker line after any blockquote markers: indentation, marker, spacing.
|
|
83
|
+
const LIST_ITEM_LINE = /^(?:[ \t]*>)*([ \t]*)([-*+]|\d{1,9}[.)])([ \t]+)\S/;
|
|
84
|
+
|
|
85
|
+
function matchesAt(pattern: RegExp, text: string, lineStart: number): boolean {
|
|
86
|
+
pattern.lastIndex = lineStart;
|
|
87
|
+
return pattern.test(text);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Column width of leading whitespace, tabs to the next multiple of four. */
|
|
91
|
+
function indentWidth(whitespace: string): number {
|
|
92
|
+
let width = 0;
|
|
93
|
+
for (const ch of whitespace) width = ch === "\t" ? width + 4 - (width % 4) : width + 1;
|
|
94
|
+
return width;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Whether the fence line at `lineStart`, indented `indent` columns, is a real
|
|
99
|
+
* fence: at most three columns in, or inside a list item whose content column
|
|
100
|
+
* it sits within three columns of. Deeper lines elsewhere are indented code.
|
|
101
|
+
*/
|
|
102
|
+
function fenceIndentValid(text: string, lineStart: number, indent: number): boolean {
|
|
103
|
+
if (indent <= 3) return true;
|
|
104
|
+
for (let end = lineStart - 1; end > 0;) {
|
|
105
|
+
const start = text.lastIndexOf("\n", end - 1) + 1;
|
|
106
|
+
const line = text.slice(start, end);
|
|
107
|
+
end = start - 1;
|
|
108
|
+
if (!line.trim()) continue;
|
|
109
|
+
const lineIndent = indentWidth(/^(?:[ \t]*>)*([ \t]*)/.exec(line)![1]);
|
|
110
|
+
if (lineIndent >= indent) continue;
|
|
111
|
+
const item = LIST_ITEM_LINE.exec(line);
|
|
112
|
+
if (item) {
|
|
113
|
+
const content = indentWidth(item[1]) + item[2].length + indentWidth(item[3]);
|
|
114
|
+
return indent >= content && indent <= content + 3;
|
|
115
|
+
}
|
|
116
|
+
// A less indented line that is no list item: only a list item's continuation text keeps looking.
|
|
117
|
+
if (lineIndent === 0) return false;
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Whether a code fence is still open at `end` of `text`. */
|
|
123
|
+
function fenceOpenAt(text: string, end: number): boolean {
|
|
124
|
+
let open: string | undefined;
|
|
125
|
+
FENCE_LINE.lastIndex = 0;
|
|
126
|
+
for (let match = FENCE_LINE.exec(text); match !== null && match.index < end; match = FENCE_LINE.exec(text)) {
|
|
127
|
+
const [, indent, marker, rest] = match;
|
|
128
|
+
if (!fenceIndentValid(text, match.index, indentWidth(indent))) continue;
|
|
129
|
+
if (open === undefined) {
|
|
130
|
+
// A backtick fence's info string cannot hold a backtick (that line is inline code).
|
|
131
|
+
if (marker[0] !== "`" || !rest.includes("`")) open = marker;
|
|
132
|
+
} else if (marker[0] === open[0] && marker.length >= open.length && !rest.trim()) {
|
|
133
|
+
open = undefined;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return open !== undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Streaming `md` source for a native terminal, minus a still-growing table row:
|
|
141
|
+
* the unterminated last line when a GFM table (a run of pipe lines holding a
|
|
142
|
+
* header and its delimiter row, outer pipes optional) sits right above it,
|
|
143
|
+
* outside any open code fence. GFM continues a table on any non-blank line, so
|
|
144
|
+
* that line is a row whatever it holds so far. The terminal sizes columns by
|
|
145
|
+
* every row it gets, so a half-streamed row (an unclosed link shows its whole
|
|
146
|
+
* URL) would rewrap the table each frame, and TSP cannot ask it to skip one.
|
|
147
|
+
* The result keeps the newline before the held line, so it is a prefix of
|
|
148
|
+
* `text` and of the result for every append to it: streaming stays
|
|
149
|
+
* append-only. Pass untrimmed source: trimming first drops the newline that
|
|
150
|
+
* marks the last row finished, so that row would be held too.
|
|
151
|
+
*/
|
|
152
|
+
export function holdPartialTableRow(text: string): string {
|
|
153
|
+
const lastBreak = text.lastIndexOf("\n");
|
|
154
|
+
if (lastBreak <= 0 || lastBreak === text.length - 1) return text;
|
|
155
|
+
// Walk up the run of pipe lines above the growing row to a delimiter row with a header above it.
|
|
156
|
+
let lineStart = text.lastIndexOf("\n", lastBreak - 1) + 1;
|
|
157
|
+
let table = false;
|
|
158
|
+
while (matchesAt(PIPE_IN_LINE_AT, text, lineStart)) {
|
|
159
|
+
const above = lineStart > 0 ? text.lastIndexOf("\n", lineStart - 2) + 1 : -1;
|
|
160
|
+
if (matchesAt(DELIMITER_ROW_AT, text, lineStart)) {
|
|
161
|
+
table = above >= 0 && matchesAt(PIPE_IN_LINE_AT, text, above);
|
|
162
|
+
lineStart = above;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
if (above < 0) break;
|
|
166
|
+
lineStart = above;
|
|
167
|
+
}
|
|
168
|
+
if (!table || fenceOpenAt(text, lineStart)) return text;
|
|
169
|
+
return text.slice(0, lastBreak + 1);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Index of the cell a streaming table's last row is still writing: the cell
|
|
174
|
+
* after its last `|`, or -1 when a newline or trailing `|` closed the row.
|
|
175
|
+
* `raw` is the table token's source; GFM splits cells on every unescaped pipe.
|
|
176
|
+
*/
|
|
177
|
+
function growingTableCell(raw: string): number {
|
|
178
|
+
if (raw.endsWith("\n")) return -1;
|
|
179
|
+
const line = raw.slice(raw.lastIndexOf("\n") + 1).trimEnd();
|
|
180
|
+
if (UNESCAPED_PIPE_END.test(line)) return -1;
|
|
181
|
+
const pipes = line.match(UNESCAPED_PIPE)?.length ?? 0;
|
|
182
|
+
return matchesAt(TABLE_LINE_AT, line, 0) ? pipes - 1 : pipes;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const MATH_CLOSERS: Record<MathOpener, string> = { $: "$", $$: "$$", "\\(": "\\)", "\\[": "\\]" };
|
|
186
|
+
// The end of a math body that is not yet valid LaTeX: a half-typed command, a lone `\`, or a dangling `^`/`_`.
|
|
187
|
+
const PARTIAL_LATEX_TAIL = /(?:\\[A-Za-z]*|[\^_])$/;
|
|
188
|
+
// A body opened by anything but `$$` needs one of these: `$5` is a price and `\[docs]` an escaped bracket until
|
|
189
|
+
// LaTeX shows up, so they stay literal until they close.
|
|
190
|
+
const LATEX_HINT = /[\\^_{}]/;
|
|
191
|
+
// Math openers tried from the end of the run; the one being written is almost always the last.
|
|
192
|
+
const MATH_OPENER_TRIES = 3;
|
|
193
|
+
|
|
194
|
+
/** Whether the character at `at` follows an odd run of backslashes. */
|
|
195
|
+
function escapedAt(text: string, at: number): boolean {
|
|
196
|
+
let slashes = 0;
|
|
197
|
+
while (at - slashes - 1 >= 0 && text[at - slashes - 1] === "\\") slashes++;
|
|
198
|
+
return slashes % 2 === 1;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Where `text` is inline prose rather than code: the closed code spans, and
|
|
203
|
+
* the start of a backtick run no later run closes. While streaming, that run
|
|
204
|
+
* is a code span still being written, so nothing after it is prose.
|
|
205
|
+
*/
|
|
206
|
+
class InlineCode {
|
|
207
|
+
readonly #spans: number[] = [];
|
|
208
|
+
readonly #openFrom: number;
|
|
209
|
+
|
|
210
|
+
constructor(text: string) {
|
|
211
|
+
let at = text.indexOf("`");
|
|
212
|
+
let openFrom = text.length;
|
|
213
|
+
while (at >= 0) {
|
|
214
|
+
let runEnd = at;
|
|
215
|
+
while (text[runEnd] === "`") runEnd++;
|
|
216
|
+
if (escapedAt(text, at) && runEnd - at === 1) {
|
|
217
|
+
at = text.indexOf("`", runEnd);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const length = runEnd - at;
|
|
221
|
+
let close = text.indexOf("`", runEnd);
|
|
222
|
+
let closeEnd = close;
|
|
223
|
+
while (close >= 0) {
|
|
224
|
+
closeEnd = close;
|
|
225
|
+
while (text[closeEnd] === "`") closeEnd++;
|
|
226
|
+
if (closeEnd - close === length) break;
|
|
227
|
+
close = text.indexOf("`", closeEnd);
|
|
228
|
+
}
|
|
229
|
+
if (close < 0) {
|
|
230
|
+
openFrom = at;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
this.#spans.push(at, closeEnd);
|
|
234
|
+
at = text.indexOf("`", closeEnd);
|
|
235
|
+
}
|
|
236
|
+
this.#openFrom = openFrom;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Whether `at` is prose: outside every code span, and before any span still open. */
|
|
240
|
+
prose(at: number): boolean {
|
|
241
|
+
if (at >= this.#openFrom) return false;
|
|
242
|
+
for (let i = 0; i < this.#spans.length; i += 2) {
|
|
243
|
+
if (at >= this.#spans[i]! && at < this.#spans[i + 1]!) return false;
|
|
244
|
+
}
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** `text` with its trailing open link closed, or `undefined` when its end is not inside a link destination. */
|
|
250
|
+
function closeOpenLink(text: string, code: () => InlineCode): string | undefined {
|
|
251
|
+
const destination = text.lastIndexOf("](");
|
|
252
|
+
if (destination < 0 || escapedAt(text, destination)) return undefined;
|
|
253
|
+
// A destination may hold balanced parentheses; `<…>` destinations and titles are not guessed.
|
|
254
|
+
if (text[destination + 2] === "<") return undefined;
|
|
255
|
+
let depth = 1;
|
|
256
|
+
for (let i = destination + 2; i < text.length; i++) {
|
|
257
|
+
const ch = text[i];
|
|
258
|
+
if (ch === "\\") i++;
|
|
259
|
+
else if (ch === "(") depth++;
|
|
260
|
+
else if (ch === ")" && --depth === 0) return undefined;
|
|
261
|
+
else if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r") return undefined;
|
|
262
|
+
}
|
|
263
|
+
let nested = 0;
|
|
264
|
+
let label = -1;
|
|
265
|
+
for (let i = destination - 1; i >= 0; i--) {
|
|
266
|
+
const ch = text[i];
|
|
267
|
+
if ((ch !== "[" && ch !== "]") || escapedAt(text, i)) continue;
|
|
268
|
+
if (ch === "]") nested++;
|
|
269
|
+
else if (nested > 0) nested--;
|
|
270
|
+
else {
|
|
271
|
+
label = i;
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// An image keeps its literal form: its alt text and source are not a link.
|
|
276
|
+
if (label < 0 || (text[label - 1] === "!" && !escapedAt(text, label - 1))) return undefined;
|
|
277
|
+
const spans = code();
|
|
278
|
+
if (!spans.prose(label) || !spans.prose(destination)) return undefined;
|
|
279
|
+
return text + ")".repeat(depth);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** `text` with its trailing open math span closed in its last valid form, or `undefined`. */
|
|
283
|
+
function closeOpenMath(text: string, code: () => InlineCode): string | undefined {
|
|
284
|
+
let from = text.length;
|
|
285
|
+
for (let tries = 0; tries < MATH_OPENER_TRIES && from > 0;) {
|
|
286
|
+
const at = Math.max(
|
|
287
|
+
text.lastIndexOf("$", from - 1),
|
|
288
|
+
text.lastIndexOf("\\(", from - 1),
|
|
289
|
+
text.lastIndexOf("\\[", from - 1),
|
|
290
|
+
);
|
|
291
|
+
if (at < 0) return undefined;
|
|
292
|
+
from = at;
|
|
293
|
+
const opener = mathOpenerAt(text, at);
|
|
294
|
+
// The second `$` of a `$$` opener is not an opener of its own.
|
|
295
|
+
if (opener === undefined || (opener === "$" && text[at - 1] === "$")) continue;
|
|
296
|
+
tries++;
|
|
297
|
+
if (!code().prose(at)) continue;
|
|
298
|
+
const bodyStart = at + opener.length;
|
|
299
|
+
let written = text.slice(bodyStart);
|
|
300
|
+
// A `$$` span whose closer is half written ends in that closer's first `$`.
|
|
301
|
+
if (opener === "$$" && written.endsWith("$") && !escapedAt(text, text.length - 1)) written = written.slice(0, -1);
|
|
302
|
+
const body = written.replace(PARTIAL_LATEX_TAIL, "");
|
|
303
|
+
if (!body.trim() || (opener !== "$$" && !LATEX_HINT.test(written))) continue;
|
|
304
|
+
let depth = 0;
|
|
305
|
+
for (let i = 0; i < body.length; i++) {
|
|
306
|
+
if (body[i] === "\\") i++;
|
|
307
|
+
else if (body[i] === "{") depth++;
|
|
308
|
+
else if (body[i] === "}" && depth > 0) depth--;
|
|
309
|
+
}
|
|
310
|
+
const closed = `${text.slice(0, bodyStart)}${body}${"}".repeat(depth)}${MATH_CLOSERS[opener]}`;
|
|
311
|
+
// The grammar decides: escaped, currency-like, or space-padded openers stay literal.
|
|
312
|
+
if (mathSpanAt(closed, at)?.end === closed.length) return closed;
|
|
313
|
+
}
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* `text`, one inline run whose end is still streaming, with a trailing open
|
|
319
|
+
* link or math span closed the way the finished text most likely closes it,
|
|
320
|
+
* or `undefined` when nothing at its end is open. A link gains its missing
|
|
321
|
+
* `)` (one per open parenthesis in its destination), so it renders as the
|
|
322
|
+
* link it becomes instead of literal brackets and URL. A math body is cut back
|
|
323
|
+
* to its last valid form (a half-typed command, dangling `^`/`_`, or the first
|
|
324
|
+
* `$` of a `$$` closer dropped, open braces closed) and closed, so the
|
|
325
|
+
* rendered math grows in place instead of raw LaTeX turning into symbols at
|
|
326
|
+
* the closer. A `$` body needs a LaTeX character, so a price (`$5 per`) is
|
|
327
|
+
* never speculated. Only prose is: escaped openers and anything inside a code
|
|
328
|
+
* span, closed or still being written, stay literal.
|
|
329
|
+
*/
|
|
330
|
+
export function speculativeInlineClose(text: string): string | undefined {
|
|
331
|
+
let spans: InlineCode | undefined;
|
|
332
|
+
const code = () => (spans ??= new InlineCode(text));
|
|
333
|
+
return closeOpenLink(text, code) ?? closeOpenMath(text, code);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Streaming `md` source for a native terminal with the open link or math span
|
|
338
|
+
* at its end speculatively closed ({@link speculativeInlineClose}), as the TUI
|
|
339
|
+
* renders it. Only the last block is read (a span cannot cross a blank line),
|
|
340
|
+
* and source inside an open code fence is left as it is. Each frame rewrites
|
|
341
|
+
* only the guessed closers at the end, which the reconciler sends as a small
|
|
342
|
+
* `splice`.
|
|
343
|
+
*/
|
|
344
|
+
export function speculativeStreamingSource(text: string): string {
|
|
345
|
+
const blockBreak = text.lastIndexOf("\n\n");
|
|
346
|
+
const blockStart = blockBreak < 0 ? 0 : blockBreak + 2;
|
|
347
|
+
const closed = speculativeInlineClose(text.slice(blockStart));
|
|
348
|
+
if (closed === undefined || fenceOpenAt(text, text.length)) return text;
|
|
349
|
+
return text.slice(0, blockStart) + closed;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** `raw` with its last `from` replaced by `to`, or `undefined` when `from` is not in it. */
|
|
353
|
+
function replaceLast(raw: string, from: string, to: string): string | undefined {
|
|
354
|
+
const at = raw.lastIndexOf(from);
|
|
355
|
+
return at < 0 ? undefined : raw.slice(0, at) + to + raw.slice(at + from.length);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* A copy of `token`, the last block of a streaming render, whose still-written
|
|
360
|
+
* inline run is speculatively closed ({@link speculativeInlineClose}), or
|
|
361
|
+
* `undefined` when nothing in it is open. Follows the run into the last list
|
|
362
|
+
* item, blockquote child, or the open cell of a table's last row. `raw` is
|
|
363
|
+
* rewritten to match, so row caches keyed on it never mistake the copy for
|
|
364
|
+
* the source as it stands.
|
|
365
|
+
*/
|
|
366
|
+
function speculateStreamingToken(token: Token): Token | undefined {
|
|
367
|
+
switch (token.type) {
|
|
368
|
+
case "paragraph":
|
|
369
|
+
case "heading":
|
|
370
|
+
case "text": {
|
|
371
|
+
const closed = speculativeInlineClose(token.text);
|
|
372
|
+
const raw = closed === undefined ? undefined : replaceLast(token.raw, token.text, closed);
|
|
373
|
+
if (closed === undefined || raw === undefined) return undefined;
|
|
374
|
+
return { ...token, raw, text: closed, tokens: lexInlineTokens(closed) };
|
|
375
|
+
}
|
|
376
|
+
case "blockquote": {
|
|
377
|
+
const children: Token[] = token.tokens ?? [];
|
|
378
|
+
const inner = children.at(-1);
|
|
379
|
+
const patched = inner && speculateStreamingToken(inner);
|
|
380
|
+
const raw = inner && patched && replaceLast(token.raw, inner.raw, patched.raw);
|
|
381
|
+
if (!patched || raw === undefined) return undefined;
|
|
382
|
+
return { ...token, raw, tokens: [...children.slice(0, -1), patched] };
|
|
383
|
+
}
|
|
384
|
+
case "list": {
|
|
385
|
+
const items: Tokens.ListItem[] = token.items;
|
|
386
|
+
const item = items.at(-1);
|
|
387
|
+
const inner = item?.tokens.at(-1);
|
|
388
|
+
if (!item || !inner) return undefined;
|
|
389
|
+
const patched = speculateStreamingToken(inner);
|
|
390
|
+
const itemRaw = patched && replaceLast(item.raw, inner.raw, patched.raw);
|
|
391
|
+
const raw = itemRaw === undefined ? undefined : replaceLast(token.raw, item.raw, itemRaw);
|
|
392
|
+
if (!patched || itemRaw === undefined || raw === undefined) return undefined;
|
|
393
|
+
const grown = { ...item, raw: itemRaw, tokens: [...item.tokens.slice(0, -1), patched] };
|
|
394
|
+
return { ...token, raw, items: [...items.slice(0, -1), grown] };
|
|
395
|
+
}
|
|
396
|
+
case "table": {
|
|
397
|
+
const rows: Tokens.TableCell[][] = token.rows;
|
|
398
|
+
const open = growingTableCell(token.raw);
|
|
399
|
+
const row = rows.at(-1);
|
|
400
|
+
const cell = open >= 0 ? row?.[open] : undefined;
|
|
401
|
+
if (!row || !cell) return undefined;
|
|
402
|
+
const closed = speculativeInlineClose(cell.text);
|
|
403
|
+
const raw = closed === undefined ? undefined : replaceLast(token.raw, cell.text, closed);
|
|
404
|
+
if (closed === undefined || raw === undefined) return undefined;
|
|
405
|
+
const cells = row.map((c, i) => (i === open ? { ...c, text: closed, tokens: lexInlineTokens(closed) } : c));
|
|
406
|
+
return { ...token, raw, rows: [...rows.slice(0, -1), cells] };
|
|
407
|
+
}
|
|
408
|
+
default:
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
55
413
|
/** The longest suffix of `text` a future append could still complete into a
|
|
56
414
|
* full `\x1b]8;[^\x07\x1b]*\x1b\\` match: the last `\x1b]8;` plus clean
|
|
57
415
|
* body (or that plus the pending ST-ESC `\x1b`), or a strict prefix of the
|
|
@@ -1421,6 +1779,33 @@ function loosenListItem(item: Tokens.ListItem): Tokens.ListItem {
|
|
|
1421
1779
|
};
|
|
1422
1780
|
}
|
|
1423
1781
|
|
|
1782
|
+
const REFERENCE_LABEL_RE = /\[[^[\]]*[^\s[\]][^[\]]*\]/;
|
|
1783
|
+
const LABEL_FREE_TOKENS = new Set(["checkbox", "code", "codespan", "escape", "html", "image", "math"]);
|
|
1784
|
+
|
|
1785
|
+
function labelSource(tokens: readonly Token[]): string {
|
|
1786
|
+
let source = "";
|
|
1787
|
+
for (const token of tokens) {
|
|
1788
|
+
if (token.type === "list") {
|
|
1789
|
+
for (const item of (token as Tokens.List).items) source += `${labelSource(item.tokens)}\n\n`;
|
|
1790
|
+
} else if (token.type === "table") {
|
|
1791
|
+
const table = token as TableToken;
|
|
1792
|
+
for (const cell of [...table.header, ...table.rows.flat()]) source += `${labelSource(cell.tokens ?? [])}\n\n`;
|
|
1793
|
+
} else if ("tokens" in token && token.tokens) {
|
|
1794
|
+
source += labelSource(token.tokens);
|
|
1795
|
+
} else {
|
|
1796
|
+
source += LABEL_FREE_TOKENS.has(token.type) ? " " : token.raw;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
return source;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
/** Whether a reference definition arriving later could still turn part of `item` into a link. */
|
|
1803
|
+
function hasUnresolvedReferenceLabel(item: Tokens.ListItem): boolean {
|
|
1804
|
+
return labelSource(item.tokens)
|
|
1805
|
+
.split(BLANK_LINE_RE)
|
|
1806
|
+
.some(block => REFERENCE_LABEL_RE.test(block));
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1424
1809
|
function formatHyperlink(text: string, target: string): string {
|
|
1425
1810
|
if (!TERMINAL.hyperlinks || !target) {
|
|
1426
1811
|
return text;
|
|
@@ -1787,6 +2172,7 @@ interface StreamingListCache {
|
|
|
1787
2172
|
start: number;
|
|
1788
2173
|
tailStart: number;
|
|
1789
2174
|
stableCount: number;
|
|
2175
|
+
referenceStop?: number;
|
|
1790
2176
|
list: Tokens.List;
|
|
1791
2177
|
}
|
|
1792
2178
|
/**
|
|
@@ -1874,6 +2260,13 @@ export class Markdown implements Component {
|
|
|
1874
2260
|
// The frozen prefix's leading run in front of the first of those openers:
|
|
1875
2261
|
// no append can change its tokens, so getLastRenderStableText publishes it.
|
|
1876
2262
|
#streamSettledText?: string;
|
|
2263
|
+
// The text in front of the newest top-level item of a list that is still
|
|
2264
|
+
// streaming as the last block. Once an item's marker line has ended, no
|
|
2265
|
+
// append can add to the items in front of it, and #renderList draws each
|
|
2266
|
+
// item from its own tokens alone, so their rows are final. Kept while later
|
|
2267
|
+
// texts extend it, so the published prefix never shrinks when the list
|
|
2268
|
+
// stops being the last block.
|
|
2269
|
+
#streamListSettledText?: string;
|
|
1877
2270
|
// The prefix before a rewind that only a closer on the still-growing last
|
|
1878
2271
|
// line caused. Once a later text holds no closer line after it, that
|
|
1879
2272
|
// prefix is right again, so #lexTokens puts it back.
|
|
@@ -2044,21 +2437,27 @@ export class Markdown implements Component {
|
|
|
2044
2437
|
const cached = this.#native;
|
|
2045
2438
|
if (cached?.text === this.#text && cached.stream === stream) return cached.node;
|
|
2046
2439
|
// Sources are model text; OSC 8 / SGR bytes a caller spliced in are the terminal's to style, not ours.
|
|
2047
|
-
const
|
|
2048
|
-
const described =
|
|
2440
|
+
const plain = this.#text.includes("\x1b") ? plainText(this.#text) : this.#text;
|
|
2441
|
+
const described = stream
|
|
2442
|
+
? md(speculativeStreamingSource(holdPartialTableRow(plain)), { stream: true })
|
|
2443
|
+
: md(plain);
|
|
2049
2444
|
this.#native = { text: this.#text, stream, node: described };
|
|
2050
2445
|
return described;
|
|
2051
2446
|
}
|
|
2052
2447
|
|
|
2053
2448
|
/**
|
|
2054
|
-
* Width-independent source prefix of the last render
|
|
2055
|
-
*
|
|
2056
|
-
* display-math opener whose block an append could still close
|
|
2449
|
+
* Width-independent source prefix of the last render that no append can
|
|
2450
|
+
* change: up to a frozen Markdown block boundary, stopping in front of a
|
|
2451
|
+
* display-math opener whose block an append could still close, or, while a
|
|
2452
|
+
* list streams as the last block, up to its newest top-level item. Only
|
|
2057
2453
|
* meaningful while streaming (transient render cache on); grows
|
|
2058
2454
|
* monotonically under append-only `setText`.
|
|
2059
2455
|
*/
|
|
2060
2456
|
getLastRenderStableText(): string {
|
|
2061
|
-
|
|
2457
|
+
if (!this.#transientRenderCache) return "";
|
|
2458
|
+
const settled = this.#streamSettledText ?? "";
|
|
2459
|
+
const list = this.#streamListSettledText;
|
|
2460
|
+
return list !== undefined && list.length > settled.length ? list : settled;
|
|
2062
2461
|
}
|
|
2063
2462
|
|
|
2064
2463
|
get transientRenderCache(): boolean {
|
|
@@ -2229,6 +2628,9 @@ export class Markdown implements Component {
|
|
|
2229
2628
|
#checkpointStreamingList(text: string, tokens: Token[]): void {
|
|
2230
2629
|
const previous = this.#streamingListCache;
|
|
2231
2630
|
this.#streamingListCache = undefined;
|
|
2631
|
+
if (this.#streamListSettledText !== undefined && !text.startsWith(this.#streamListSettledText)) {
|
|
2632
|
+
this.#streamListSettledText = undefined;
|
|
2633
|
+
}
|
|
2232
2634
|
let index = tokens.length - 1;
|
|
2233
2635
|
while (tokens[index]?.type === "space") index--;
|
|
2234
2636
|
const token = tokens[index];
|
|
@@ -2243,6 +2645,7 @@ export class Markdown implements Component {
|
|
|
2243
2645
|
let cursor = canResume ? previous.tailStart : start;
|
|
2244
2646
|
let tailStart = start;
|
|
2245
2647
|
let stableCount = 0;
|
|
2648
|
+
let referenceStop = canResume ? previous.referenceStop : undefined;
|
|
2246
2649
|
const indent = /^ */.exec(list.raw)![0].length;
|
|
2247
2650
|
for (let i = canResume ? previous.stableCount : 0; i < list.items.length; i++) {
|
|
2248
2651
|
const item = list.items[i]!;
|
|
@@ -2254,9 +2657,34 @@ export class Markdown implements Component {
|
|
|
2254
2657
|
tailStart = position;
|
|
2255
2658
|
stableCount = i;
|
|
2256
2659
|
}
|
|
2660
|
+
if (referenceStop === undefined && hasUnresolvedReferenceLabel(item)) referenceStop = tailStart;
|
|
2257
2661
|
cursor = position + item.raw.length;
|
|
2258
2662
|
}
|
|
2259
|
-
|
|
2663
|
+
// Items from stableCount on are scanned again next time and may still
|
|
2664
|
+
// resolve their own labels, so only a finished item's label is kept.
|
|
2665
|
+
if (referenceStop !== undefined && referenceStop >= tailStart) referenceStop = undefined;
|
|
2666
|
+
this.#streamingListCache = {
|
|
2667
|
+
text,
|
|
2668
|
+
before: tokens.slice(0, index),
|
|
2669
|
+
start,
|
|
2670
|
+
tailStart,
|
|
2671
|
+
stableCount,
|
|
2672
|
+
referenceStop,
|
|
2673
|
+
list,
|
|
2674
|
+
};
|
|
2675
|
+
// No append can add to an item once a later one has begun, but a later
|
|
2676
|
+
// reference definition can still restyle a bracketed label in it. The
|
|
2677
|
+
// list must also start right at the settled boundary: in front of it may
|
|
2678
|
+
// sit a display-math opener whose closer, arriving after the list, would
|
|
2679
|
+
// turn the whole run, list included, into one math block.
|
|
2680
|
+
const settledEnd = referenceStop ?? tailStart;
|
|
2681
|
+
if (
|
|
2682
|
+
settledEnd > start &&
|
|
2683
|
+
(this.#streamSettledText?.length ?? 0) === start &&
|
|
2684
|
+
settledEnd > (this.#streamListSettledText?.length ?? 0)
|
|
2685
|
+
) {
|
|
2686
|
+
this.#streamListSettledText = text.slice(0, settledEnd);
|
|
2687
|
+
}
|
|
2260
2688
|
}
|
|
2261
2689
|
|
|
2262
2690
|
/**
|
|
@@ -2269,6 +2697,7 @@ export class Markdown implements Component {
|
|
|
2269
2697
|
this.#streamPrefixTokens = undefined;
|
|
2270
2698
|
this.#streamPrefixOpeners = NO_OPENERS;
|
|
2271
2699
|
this.#streamSettledText = undefined;
|
|
2700
|
+
this.#streamListSettledText = undefined;
|
|
2272
2701
|
this.#streamPrefixLineCache = undefined;
|
|
2273
2702
|
this.#tailRowCache = undefined;
|
|
2274
2703
|
this.#listItemRowsCache = undefined;
|
|
@@ -2520,7 +2949,9 @@ export class Markdown implements Component {
|
|
|
2520
2949
|
// into a table on a marker-free delta (`| --` + `--- | -`).
|
|
2521
2950
|
!FAST_TABLE_DELIM_ROW_RE.test(grownLastLine) &&
|
|
2522
2951
|
(!markerDelta || (!this.#lastTailCapture?.open && !pairSeamHazard && !inlineHasOpen(deltaTokens!))) &&
|
|
2523
|
-
!FAST_URL_ANYWHERE_RE.test(seamWindow)
|
|
2952
|
+
!FAST_URL_ANYWHERE_RE.test(seamWindow) &&
|
|
2953
|
+
// A link or math span the grown row leaves open renders speculatively closed: re-lex.
|
|
2954
|
+
speculativeInlineClose(grownLastLine) === undefined
|
|
2524
2955
|
) {
|
|
2525
2956
|
// Same text paths a full re-lex applies: real pipeline for marker
|
|
2526
2957
|
// deltas, plain swatch/entity render for inert deltas.
|
|
@@ -2624,8 +3055,12 @@ export class Markdown implements Component {
|
|
|
2624
3055
|
}
|
|
2625
3056
|
}
|
|
2626
3057
|
|
|
2627
|
-
// Parse markdown to HTML-like tokens
|
|
2628
|
-
|
|
3058
|
+
// Parse markdown to HTML-like tokens. While streaming, the block still
|
|
3059
|
+
// being written renders its open link or math span as already closed.
|
|
3060
|
+
const lexed = this.#lexTokens(normalizedText);
|
|
3061
|
+
const tail = lexed.at(-1);
|
|
3062
|
+
const speculated = this.transientRenderCache && tail ? speculateStreamingToken(tail) : undefined;
|
|
3063
|
+
const tokens = speculated ? [...lexed.slice(0, -1), speculated] : lexed;
|
|
2629
3064
|
let contentLines: string[];
|
|
2630
3065
|
this.#activeRenderSignature = signature;
|
|
2631
3066
|
try {
|
|
@@ -2652,6 +3087,8 @@ export class Markdown implements Component {
|
|
|
2652
3087
|
|
|
2653
3088
|
const fastEligible =
|
|
2654
3089
|
this.transientRenderCache &&
|
|
3090
|
+
// A speculated frame's rows come from source the stream does not hold yet.
|
|
3091
|
+
speculated === undefined &&
|
|
2655
3092
|
contentLines.length > 0 &&
|
|
2656
3093
|
// B+ invariant: only the FINAL #renderContentLines call captures;
|
|
2657
3094
|
// the all-cache-hit prefix path clears #lastTailCapture at its top.
|
|
@@ -4042,8 +4479,21 @@ export class Markdown implements Component {
|
|
|
4042
4479
|
naturalWidths[i] = Math.max(...headerLineWidths, 0);
|
|
4043
4480
|
minWordWidths[i] = Math.max(1, this.#getLongestWordWidth(headerText, maxUnbrokenWordWidth));
|
|
4044
4481
|
}
|
|
4045
|
-
|
|
4482
|
+
// While streaming, the last row's open cell is still growing (an unclosed
|
|
4483
|
+
// link even shows its whole URL): sizing columns by it would rewrap every
|
|
4484
|
+
// row above it on each frame. Lay that cell out in the measured widths
|
|
4485
|
+
// instead; it counts once a `|` or newline closes it. The row's closed
|
|
4486
|
+
// cells are final and count already. The first data row has no finished
|
|
4487
|
+
// rows to borrow from, so it counts whole.
|
|
4488
|
+
const openCell =
|
|
4489
|
+
this.#transientRenderCache && !this.#renderingStablePrefix && token.rows.length > 1
|
|
4490
|
+
? growingTableCell(token.raw)
|
|
4491
|
+
: -1;
|
|
4492
|
+
const lastRow = token.rows.length - 1;
|
|
4493
|
+
for (let rowIndex = 0; rowIndex <= lastRow; rowIndex++) {
|
|
4494
|
+
const row = token.rows[rowIndex];
|
|
4046
4495
|
for (let i = 0; i < row.length; i++) {
|
|
4496
|
+
if (rowIndex === lastRow && i === openCell) continue;
|
|
4047
4497
|
const cellText = this.#renderInlineTokens(row[i].tokens || [], styleContext);
|
|
4048
4498
|
const cellLineWidths = this.#terminalLineWidths(cellText);
|
|
4049
4499
|
naturalWidths[i] = Math.max(naturalWidths[i] || 0, ...cellLineWidths);
|
|
@@ -4,6 +4,7 @@ import { fuzzyFilter } from "../fuzzy";
|
|
|
4
4
|
import { getKeybindings } from "../keybindings";
|
|
5
5
|
import { extractPrintableText } from "../keys";
|
|
6
6
|
import type { MouseRoutable, SgrMouseEvent } from "../mouse";
|
|
7
|
+
import { slugify } from "@linxiraos/pi-utils";
|
|
7
8
|
import type { TspPrefsControl, TspPrefsRow, TspPrefsSection, TspProps, TspSpan } from "@linxiraos/pi-wire";
|
|
8
9
|
import { col, node, span } from "../native/describe";
|
|
9
10
|
import { sameItems, sameProps } from "../native/memo";
|
|
@@ -78,10 +79,7 @@ export interface PrefsSectionRef {
|
|
|
78
79
|
|
|
79
80
|
/** The section id for a section title (`"Status Line"` → `"status-line"`). */
|
|
80
81
|
export function prefsSectionId(title: string): string {
|
|
81
|
-
return title
|
|
82
|
-
.toLowerCase()
|
|
83
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
84
|
-
.replace(/^-|-$/g, "");
|
|
82
|
+
return slugify(title);
|
|
85
83
|
}
|
|
86
84
|
|
|
87
85
|
/** A setting as a native settings row, given its typed control; `undefined` for headings. */
|
package/src/components/text.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ import type { Component } from "../tui";
|
|
|
12
12
|
export declare class Text implements Component {
|
|
13
13
|
#private;
|
|
14
14
|
setIgnoreTight(ignore: boolean): this;
|
|
15
|
+
/** Override row wrapping when the source carries structural prefixes. */
|
|
16
|
+
setWrapLine(wrapLine: (line: string, width: number) => string[]): this;
|
|
15
17
|
constructor(text?: string, paddingX?: number, paddingY?: number, customBgFn?: (text: string) => string);
|
|
16
18
|
/** Return bounded text and layout state for debug inspection. */
|
|
17
19
|
debugState(): Record<string, unknown>;
|
package/src/components/text.ts
CHANGED
|
@@ -30,6 +30,7 @@ export class Text implements Component {
|
|
|
30
30
|
#customBgFn?: (text: string) => string;
|
|
31
31
|
#styleFn?: (text: string) => string;
|
|
32
32
|
#ignoreTight = false;
|
|
33
|
+
#wrapLine?: (line: string, width: number) => string[];
|
|
33
34
|
|
|
34
35
|
setIgnoreTight(ignore: boolean): this {
|
|
35
36
|
if (this.#ignoreTight === ignore) return this;
|
|
@@ -37,6 +38,12 @@ export class Text implements Component {
|
|
|
37
38
|
this.invalidate();
|
|
38
39
|
return this;
|
|
39
40
|
}
|
|
41
|
+
/** Override row wrapping when the source carries structural prefixes. */
|
|
42
|
+
setWrapLine(wrapLine: (line: string, width: number) => string[]): this {
|
|
43
|
+
this.#wrapLine = wrapLine;
|
|
44
|
+
this.invalidate();
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
40
47
|
|
|
41
48
|
// Cache for rendered output
|
|
42
49
|
#cachedText?: string;
|
|
@@ -162,7 +169,10 @@ export class Text implements Component {
|
|
|
162
169
|
const paddingX = this.#ignoreTight ? this.#paddingX : getPaddingX(this.#paddingX);
|
|
163
170
|
const contentWidth = Math.max(1, width - paddingX * 2);
|
|
164
171
|
// Wrap text (this preserves ANSI codes but does NOT pad)
|
|
165
|
-
const
|
|
172
|
+
const wrapLine = this.#wrapLine;
|
|
173
|
+
const wrappedLines = wrapLine
|
|
174
|
+
? normalizedText.split("\n").flatMap(line => wrapLine(line, contentWidth))
|
|
175
|
+
: wrapTextWithAnsi(normalizedText, contentWidth);
|
|
166
176
|
|
|
167
177
|
// Add margins and background to each line
|
|
168
178
|
const leftMargin = padding(paddingX);
|
package/src/debug-server.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface TuiDebugTree {
|
|
|
22
22
|
export declare class TuiDebugServer {
|
|
23
23
|
#private;
|
|
24
24
|
constructor(tui: TUI, path: string);
|
|
25
|
+
/** Resolves true once the socket accepts connections, false if listen failed or the server stopped first. */
|
|
26
|
+
get ready(): Promise<boolean>;
|
|
25
27
|
start(): void;
|
|
26
28
|
stop(): void;
|
|
27
29
|
}
|