@linxiraos/pi-tui 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +188 -125
- package/README.md +36 -0
- package/THIRD-PARTY-NOTICES.txt +55 -5
- package/dist/types/app-keybindings.d.ts +18 -8
- package/dist/types/apps/git/diff-pane.d.ts +5 -0
- package/dist/types/apps/git/git-tui.d.ts +16 -2
- package/dist/types/apps/git/state.d.ts +11 -0
- package/dist/types/autocomplete.d.ts +2 -0
- package/dist/types/charts/chart-svg.d.ts +7 -13
- package/dist/types/chat/advisor-message.d.ts +4 -1
- package/dist/types/chat/assistant-message.d.ts +9 -4
- package/dist/types/chat/figure-markdown.d.ts +7 -2
- package/dist/types/chat/messages.d.ts +3 -1
- package/dist/types/chat/read-tool-group.d.ts +2 -2
- package/dist/types/chat/table-chart.d.ts +11 -15
- package/dist/types/chat/tool-execution.d.ts +32 -20
- package/dist/types/chat/user-message.d.ts +3 -2
- package/dist/types/chrome/countdown-timer.d.ts +3 -2
- package/dist/types/chrome/diff.d.ts +2 -3
- package/dist/types/chrome/transcript-container.d.ts +2 -0
- package/dist/types/components/image.d.ts +2 -0
- package/dist/types/components/markdown.d.ts +49 -3
- package/dist/types/components/text.d.ts +2 -0
- package/dist/types/debug-server.d.ts +2 -0
- package/dist/types/keybinding-matchers.d.ts +6 -0
- package/dist/types/multiplexers/tmux.d.ts +1 -0
- package/dist/types/multiplexers/types.d.ts +6 -0
- package/dist/types/native/backend.d.ts +5 -4
- package/dist/types/native/blobs.d.ts +24 -12
- package/dist/types/overlays/agent-hub-projection.d.ts +10 -5
- package/dist/types/overlays/annotation-overlay.d.ts +19 -0
- package/dist/types/overlays/approval-dialog.d.ts +79 -0
- package/dist/types/overlays/ask-dialog.d.ts +11 -6
- package/dist/types/overlays/btw-history-panel.d.ts +2 -6
- package/dist/types/overlays/hook-editor.d.ts +7 -2
- package/dist/types/overlays/hook-input.d.ts +12 -4
- package/dist/types/overlays/job-tree.d.ts +50 -0
- package/dist/types/overlays/jobs-panel.d.ts +25 -11
- package/dist/types/overlays/login-dialog.d.ts +8 -3
- package/dist/types/overlays/mcp-add-wizard.d.ts +9 -1
- package/dist/types/overlays/model-browser.d.ts +6 -1
- package/dist/types/overlays/model-picker.d.ts +2 -0
- package/dist/types/overlays/pause-screen.d.ts +5 -0
- package/dist/types/overlays/plan-review-overlay.d.ts +4 -0
- package/dist/types/overlays/report-panel.d.ts +5 -1
- package/dist/types/overlays/reset-usage-selector.d.ts +7 -5
- package/dist/types/overlays/session-account-selector.d.ts +4 -0
- package/dist/types/overlays/session-info-overlay.d.ts +7 -3
- package/dist/types/overlays/session-selector.d.ts +3 -1
- package/dist/types/overlays/usage-dashboard.d.ts +6 -6
- package/dist/types/prompt/composer.d.ts +21 -20
- package/dist/types/prompt/custom-editor.d.ts +20 -10
- package/dist/types/prompt/image-references.d.ts +22 -0
- package/dist/types/prompt/internal-url-autocomplete.d.ts +1 -0
- package/dist/types/prompt/macos-spelling.d.ts +2 -1
- package/dist/types/prompt/prose-gate.d.ts +3 -0
- package/dist/types/prompt/queued-messages.d.ts +1 -1
- package/dist/types/prompt/typing-word.d.ts +25 -0
- package/dist/types/prompt/welcome.d.ts +26 -6
- package/dist/types/render/hanging-wrap.d.ts +7 -0
- package/dist/types/render/hyperlink.d.ts +2 -0
- package/dist/types/render/render-utils.d.ts +9 -1
- package/dist/types/render/utils.d.ts +1 -1
- package/dist/types/setup/scenes/splash.d.ts +6 -5
- package/dist/types/status-line/context-usage.d.ts +4 -1
- package/dist/types/status-line/host.d.ts +6 -4
- package/dist/types/status-line/startup.d.ts +6 -6
- package/dist/types/status-line/types.d.ts +3 -0
- package/dist/types/stdin-buffer.d.ts +7 -0
- package/dist/types/terminal-capabilities.d.ts +16 -1
- package/dist/types/terminal-multiplexer.d.ts +7 -0
- package/dist/types/terminal.d.ts +8 -4
- package/dist/types/theme/symbols.d.ts +1 -1
- package/dist/types/theme/theme.d.ts +11 -0
- package/dist/types/thinking.d.ts +5 -0
- package/dist/types/tmux.d.ts +39 -4
- package/dist/types/tools/ast-edit.d.ts +2 -2
- package/dist/types/tools/ast-grep.d.ts +2 -2
- package/dist/types/tools/bash.d.ts +6 -1
- package/dist/types/tools/edit.d.ts +2 -0
- package/dist/types/tools/eval.d.ts +53 -4
- package/dist/types/tools/find.d.ts +14 -4
- package/dist/types/tools/glob.d.ts +3 -3
- package/dist/types/tools/grep.d.ts +2 -2
- package/dist/types/tools/grouped-file-output.d.ts +9 -4
- package/dist/types/tools/native-view.d.ts +1 -1
- package/dist/types/tools/read.d.ts +32 -10
- package/dist/types/tools/renderer.d.ts +2 -0
- package/dist/types/tools/task.d.ts +31 -0
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/tui.d.ts +20 -3
- package/package.json +10 -10
- package/src/app-keybindings.d.ts +18 -8
- package/src/app-keybindings.ts +20 -8
- package/src/apps/autoresearch-dashboard.ts +444 -27
- package/src/apps/debug/raw-sse.ts +2 -2
- package/src/apps/git/diff-pane.ts +39 -41
- package/src/apps/git/git-tui.ts +80 -14
- package/src/apps/git/sidebar.ts +10 -1
- package/src/apps/git/state.ts +12 -0
- package/src/autocomplete.d.ts +2 -0
- package/src/autocomplete.ts +21 -19
- package/src/bracketed-paste.ts +9 -2
- package/src/charts/chart-svg.d.ts +7 -13
- package/src/charts/chart-svg.ts +19 -22
- package/src/chat/advisor-message.d.ts +4 -1
- package/src/chat/advisor-message.ts +77 -24
- package/src/chat/assistant-message.d.ts +9 -4
- package/src/chat/assistant-message.ts +127 -31
- package/src/chat/bash-execution.ts +7 -2
- package/src/chat/figure-markdown.d.ts +7 -2
- package/src/chat/figure-markdown.ts +10 -3
- package/src/chat/image-loading.ts +9 -6
- package/src/chat/messages.d.ts +3 -1
- package/src/chat/messages.ts +4 -1
- package/src/chat/read-tool-group.d.ts +2 -2
- package/src/chat/read-tool-group.ts +97 -42
- package/src/chat/svg-figure.ts +3 -1
- package/src/chat/svg-source.ts +6 -3
- package/src/chat/table-chart.d.ts +11 -15
- package/src/chat/table-chart.ts +24 -57
- package/src/chat/tool-execution.d.ts +32 -20
- package/src/chat/tool-execution.ts +103 -36
- package/src/chat/user-message.d.ts +3 -2
- package/src/chat/user-message.ts +54 -34
- package/src/chrome/countdown-timer.ts +3 -3
- package/src/chrome/diff.d.ts +2 -3
- package/src/chrome/diff.ts +4 -9
- package/src/chrome/transcript-container.d.ts +2 -0
- package/src/chrome/transcript-container.ts +212 -28
- package/src/components/editor.ts +12 -12
- package/src/components/image.d.ts +2 -0
- package/src/components/image.ts +54 -20
- package/src/components/markdown.d.ts +49 -3
- package/src/components/markdown.ts +461 -11
- package/src/components/settings-list.ts +2 -4
- package/src/components/text.d.ts +2 -0
- package/src/components/text.ts +11 -1
- package/src/debug-server.d.ts +2 -0
- package/src/debug-server.ts +51 -69
- package/src/hotkeys-markdown.ts +2 -0
- package/src/keybinding-matchers.d.ts +6 -0
- package/src/keybinding-matchers.ts +19 -0
- package/src/multiplexers/tmux.d.ts +1 -0
- package/src/multiplexers/tmux.ts +3 -0
- package/src/multiplexers/types.d.ts +6 -0
- package/src/multiplexers/types.ts +6 -0
- package/src/native/backend.d.ts +5 -4
- package/src/native/backend.ts +13 -5
- package/src/native/blobs.d.ts +24 -12
- package/src/native/blobs.ts +56 -7
- package/src/native/reconcile.ts +43 -21
- package/src/overlays/advisor-config.ts +259 -8
- package/src/overlays/agent-hub-projection.d.ts +10 -5
- package/src/overlays/agent-hub-projection.ts +63 -12
- package/src/overlays/annotation-overlay.ts +422 -168
- package/src/overlays/approval-dialog.ts +389 -0
- package/src/overlays/ask-dialog.ts +244 -105
- package/src/overlays/btw-history-panel.ts +208 -147
- package/src/overlays/extensions/extension-list.ts +4 -6
- package/src/overlays/hook-editor.ts +90 -32
- package/src/overlays/hook-input.ts +105 -19
- package/src/overlays/job-tree.ts +177 -0
- package/src/overlays/jobs-panel.ts +222 -142
- package/src/overlays/login-dialog.ts +201 -91
- package/src/overlays/mcp-add-wizard.ts +562 -279
- package/src/overlays/model-browser.ts +32 -7
- package/src/overlays/model-hub.ts +4 -10
- package/src/overlays/model-picker.ts +58 -21
- package/src/overlays/pause-screen.ts +81 -23
- package/src/overlays/plan-review-overlay.ts +16 -0
- package/src/overlays/plugin-selector.ts +17 -1
- package/src/overlays/queue-mode-selector.ts +10 -0
- package/src/overlays/report-panel.ts +6 -2
- package/src/overlays/reset-usage-selector.ts +130 -21
- package/src/overlays/session-account-selector.ts +36 -0
- package/src/overlays/session-info-overlay.ts +199 -76
- package/src/overlays/session-selector.ts +15 -10
- package/src/overlays/settings-selector.ts +3 -0
- package/src/overlays/show-images-selector.ts +1 -0
- package/src/overlays/snapcompact-shape-preview.ts +13 -6
- package/src/overlays/theme-selector.ts +6 -1
- package/src/overlays/thinking-selector.ts +8 -4
- package/src/overlays/tree-selector.ts +117 -2
- package/src/overlays/usage-dashboard.ts +498 -144
- package/src/prompt/composer-cache.ts +1 -1
- package/src/prompt/composer.ts +194 -74
- package/src/prompt/custom-editor.ts +168 -65
- package/src/prompt/image-references.d.ts +22 -0
- package/src/prompt/image-references.ts +79 -2
- package/src/prompt/internal-url-autocomplete.ts +1 -0
- package/src/prompt/macos-spelling.ts +28 -5
- package/src/prompt/prose-gate.ts +3 -0
- package/src/prompt/queued-messages.ts +9 -2
- package/src/prompt/tips.txt +1 -2
- package/src/prompt/typing-word.ts +59 -0
- package/src/prompt/welcome.ts +97 -45
- package/src/render/code-cell.ts +18 -5
- package/src/render/hanging-wrap.d.ts +7 -0
- package/src/render/hanging-wrap.ts +58 -0
- package/src/render/hyperlink.d.ts +2 -0
- package/src/render/hyperlink.ts +5 -0
- package/src/render/render-utils.d.ts +9 -1
- package/src/render/render-utils.ts +16 -2
- package/src/render/utils.d.ts +1 -1
- package/src/render/utils.ts +1 -1
- package/src/render/width-aware-text.ts +6 -5
- package/src/setup/scenes/splash.ts +22 -8
- package/src/status-line/component.ts +23 -10
- package/src/status-line/context-usage.ts +176 -73
- package/src/status-line/footer.ts +18 -35
- package/src/status-line/host.ts +6 -1
- package/src/status-line/segments.ts +16 -5
- package/src/status-line/startup.ts +6 -6
- package/src/status-line/types.ts +1 -0
- package/src/stdin-buffer.d.ts +7 -0
- package/src/stdin-buffer.ts +11 -1
- package/src/terminal-capabilities.d.ts +16 -1
- package/src/terminal-capabilities.ts +66 -28
- package/src/terminal-multiplexer.d.ts +7 -0
- package/src/terminal-multiplexer.ts +14 -0
- package/src/terminal.d.ts +8 -4
- package/src/terminal.ts +160 -46
- package/src/theme/symbols.d.ts +1 -1
- package/src/theme/symbols.ts +1 -0
- package/src/theme/theme-class.ts +1 -0
- package/src/theme/theme.d.ts +11 -0
- package/src/theme/theme.ts +46 -14
- package/src/thinking.d.ts +5 -0
- package/src/thinking.ts +9 -1
- package/src/tmux.d.ts +39 -4
- package/src/tmux.ts +110 -16
- package/src/tools/ask.ts +20 -9
- package/src/tools/ast-edit.d.ts +2 -2
- package/src/tools/ast-edit.ts +30 -7
- package/src/tools/ast-grep.d.ts +2 -2
- package/src/tools/ast-grep.ts +39 -35
- package/src/tools/bash.d.ts +6 -1
- package/src/tools/bash.ts +15 -9
- package/src/tools/debug.ts +2 -1
- package/src/tools/default-renderer.ts +3 -2
- package/src/tools/edit.d.ts +2 -0
- package/src/tools/edit.ts +100 -21
- package/src/tools/eval.d.ts +53 -4
- package/src/tools/eval.ts +173 -137
- package/src/tools/find.d.ts +14 -4
- package/src/tools/find.ts +71 -18
- package/src/tools/glob.d.ts +3 -3
- package/src/tools/glob.ts +51 -17
- package/src/tools/grep.d.ts +2 -2
- package/src/tools/grep.ts +99 -22
- package/src/tools/grouped-file-output.d.ts +9 -4
- package/src/tools/grouped-file-output.ts +21 -6
- package/src/tools/lsp.ts +30 -18
- package/src/tools/mcp.ts +2 -2
- package/src/tools/native-view.d.ts +1 -1
- package/src/tools/native-view.ts +2 -2
- package/src/tools/read.d.ts +32 -10
- package/src/tools/read.ts +89 -43
- package/src/tools/renderer.d.ts +2 -0
- package/src/tools/renderer.ts +2 -0
- package/src/tools/task.d.ts +31 -0
- package/src/tools/task.ts +40 -7
- package/src/tools/todo.ts +3 -2
- package/src/tools/wait.ts +16 -30
- package/src/tools/web-search.ts +2 -1
- package/src/tools/write.d.ts +2 -0
- package/src/tools/write.ts +36 -12
- package/src/tools/yield.ts +2 -2
- package/src/tui.d.ts +20 -3
- package/src/tui.ts +140 -36
- package/src/utils.ts +5 -3
- package/dist/types/tools/eval-format/code-mode-declarations.d.ts +0 -5
- package/src/tools/eval-format/code-mode-declarations.ts +0 -61
|
@@ -4,9 +4,9 @@ import { visibleWidth } from "../utils";
|
|
|
4
4
|
import type { AdvisorMessageDetails, AdvisorNote, AdvisorSeverity } from "./messages";
|
|
5
5
|
import { formatBadge, replaceTabs, type ToolUIColor, wrapTextWithAnsi } from "../render/render-utils";
|
|
6
6
|
import { Ellipsis, truncateToWidth } from "../render";
|
|
7
|
-
import {
|
|
8
|
-
import type { TspSpan } from "@linxiraos/pi-wire";
|
|
9
|
-
import { card, span, text } from "../native/describe";
|
|
7
|
+
import type { Theme } from "../theme";
|
|
8
|
+
import type { TspSpan, TspTone } from "@linxiraos/pi-wire";
|
|
9
|
+
import { card, node, span, text } from "../native/describe";
|
|
10
10
|
import { type NativeNode, type NativeUiEvent, rootToggleExpanded } from "../native/node";
|
|
11
11
|
import { plainText } from "../native/spans";
|
|
12
12
|
import { Memo } from "../native/memo";
|
|
@@ -111,13 +111,16 @@ class AdvisorNotes implements Component {
|
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/** The advisor a note came from; undefined for the implicit single ("default") advisor, which goes unlabeled. */
|
|
115
|
+
function sourceOf(entry: AdvisorNote): string | undefined {
|
|
116
|
+
return entry.advisor && entry.advisor !== "default" ? entry.advisor : undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
114
119
|
/** Wrapped, truncated rail rows for a single note; shared by both branches. */
|
|
115
120
|
function renderAdvisorNote(entry: AdvisorNote, width: number, uiTheme: Theme): string[] {
|
|
116
121
|
const badge = entry.severity ? `${formatBadge(entry.severity, severityColor(entry.severity), uiTheme)} ` : "";
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const who =
|
|
120
|
-
entry.advisor && entry.advisor !== "default" ? `${uiTheme.fg("dim", `[${replaceTabs(entry.advisor)}]`)} ` : "";
|
|
122
|
+
const source = sourceOf(entry);
|
|
123
|
+
const who = source ? `${uiTheme.fg("dim", `[${replaceTabs(source)}]`)} ` : "";
|
|
121
124
|
const age = entry.turnsAgo !== undefined ? `${uiTheme.fg("dim", `T-${entry.turnsAgo}`)} ` : "";
|
|
122
125
|
const railGlyph = uiTheme.symbol("advisor.rail");
|
|
123
126
|
const rail = uiTheme.fg(severityColor(entry.severity), railGlyph);
|
|
@@ -145,16 +148,47 @@ function renderAdvisorNote(entry: AdvisorNote, width: number, uiTheme: Theme): s
|
|
|
145
148
|
);
|
|
146
149
|
}
|
|
147
150
|
|
|
148
|
-
/**
|
|
149
|
-
|
|
151
|
+
/** A note's rank in the native callout: its severity, `note` when it has none. */
|
|
152
|
+
type AdvisorRank = AdvisorSeverity | "note";
|
|
153
|
+
|
|
154
|
+
/** Ranks worst first: the callout wears the worst rank among its notes. */
|
|
155
|
+
const RANKS: readonly AdvisorRank[] = ["blocker", "concern", "nit", "note"];
|
|
156
|
+
|
|
157
|
+
/** Each rank's native look, GitHub-alert style: card/icon tone, Tern icon name, label and its span style. */
|
|
158
|
+
const RANK_LOOK: Record<AdvisorRank, { tone: TspTone; icon: string; label: string; style: string }> = {
|
|
159
|
+
blocker: { tone: "error", icon: "octagon-alert", label: "Blocker", style: "error strong" },
|
|
160
|
+
concern: { tone: "warning", icon: "warn", label: "Concern", style: "warning strong" },
|
|
161
|
+
nit: { tone: "neutral", icon: "lightbulb", label: "Nit", style: "strong" },
|
|
162
|
+
note: { tone: "neutral", icon: "info", label: "Note", style: "strong" },
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
function rankOf(entry: AdvisorNote): AdvisorRank {
|
|
166
|
+
return entry.severity ?? "note";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** The callout's title: one note's rank (`Concern`), else a tally worst first (`1 blocker · 2 concerns`). */
|
|
170
|
+
function tallySpans(notes: readonly AdvisorNote[]): TspSpan[] {
|
|
171
|
+
if (notes.length === 1) {
|
|
172
|
+
const { label, style } = RANK_LOOK[rankOf(notes[0]!)];
|
|
173
|
+
return [span(label, style)];
|
|
174
|
+
}
|
|
150
175
|
const spans: TspSpan[] = [];
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
176
|
+
for (const rank of RANKS) {
|
|
177
|
+
const count = notes.filter(entry => rankOf(entry) === rank).length;
|
|
178
|
+
if (count === 0) continue;
|
|
179
|
+
if (spans.length > 0) spans.push(span(" · ", "dim"));
|
|
180
|
+
spans.push(span(`${count} ${rank}${count === 1 ? "" : "s"}`, RANK_LOOK[rank].style));
|
|
181
|
+
}
|
|
155
182
|
return spans;
|
|
156
183
|
}
|
|
157
184
|
|
|
185
|
+
/** Native spans for one note: its advisor when the callout mixes several, then the text. */
|
|
186
|
+
function noteSpans(entry: AdvisorNote, attribute: boolean): TspSpan[] {
|
|
187
|
+
const who = attribute ? sourceOf(entry) : undefined;
|
|
188
|
+
const body = span(plainText(entry.note));
|
|
189
|
+
return who ? [span(`${who} · `, "dim"), body] : [body];
|
|
190
|
+
}
|
|
191
|
+
|
|
158
192
|
/** The advisor card: shared by the controlled expansion getter and native toggles. */
|
|
159
193
|
export interface AdvisorMessageCard extends Component {
|
|
160
194
|
/** Transcript-wide expansion (`Ctrl+O`); clears a toggle made in the terminal. */
|
|
@@ -168,6 +202,9 @@ export interface AdvisorMessageCard extends Component {
|
|
|
168
202
|
* output (whose `thinkingText` color equals `toolOutput` in most themes):
|
|
169
203
|
* a bold `customMessageLabel` header tag (skill-card convention), a heavy
|
|
170
204
|
* rail tinted per-note severity, and the note body on the default text color.
|
|
205
|
+
* Natively it is a callout tinted by its worst note: that severity's icon and
|
|
206
|
+
* the tally head it, the advisor's name ends the head, and with several notes
|
|
207
|
+
* each leads with its own severity icon.
|
|
171
208
|
*
|
|
172
209
|
* `getExpanded` owns expansion; a native toggle overrides it for this card
|
|
173
210
|
* until the next transcript-wide change.
|
|
@@ -193,24 +230,40 @@ export function createAdvisorMessageCard(
|
|
|
193
230
|
};
|
|
194
231
|
const nativeMemo = new Memo();
|
|
195
232
|
const describeCard = (isExpanded: boolean): NativeNode => {
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
233
|
+
const look = RANK_LOOK[RANKS.find(rank => notes.some(entry => rankOf(entry) === rank)) ?? "note"];
|
|
234
|
+
const sources = new Set(notes.map(sourceOf));
|
|
235
|
+
const [sole] = sources;
|
|
236
|
+
const head = node(
|
|
237
|
+
"row",
|
|
238
|
+
{ gap: "sm", align: "center", role: "zeta.advisor.head" },
|
|
239
|
+
[
|
|
240
|
+
node("icon", { name: look.icon, tone: look.tone }),
|
|
241
|
+
text(tallySpans(notes), { grow: 1 }),
|
|
242
|
+
text([span(sources.size === 1 && sole ? sole : "Advisor", "dim")], { role: "zeta.advisor.source" }),
|
|
243
|
+
],
|
|
244
|
+
"head",
|
|
203
245
|
);
|
|
246
|
+
const attribute = sources.size > 1;
|
|
247
|
+
const body = notes.map((entry, index) => {
|
|
248
|
+
const spans = noteSpans(entry, attribute);
|
|
249
|
+
if (notes.length === 1) return text(spans, { wrap: "word", role: "zeta.advisor.note", key: `n${index}` });
|
|
250
|
+
const { tone, icon, label } = RANK_LOOK[rankOf(entry)];
|
|
251
|
+
return node(
|
|
252
|
+
"row",
|
|
253
|
+
{ gap: "sm", align: "start", role: "zeta.advisor.note", tone },
|
|
254
|
+
[node("icon", { name: icon, tone, aria: label }), text(spans, { wrap: "word", grow: 1, shrink: 1 })],
|
|
255
|
+
`n${index}`,
|
|
256
|
+
);
|
|
257
|
+
});
|
|
204
258
|
return card(
|
|
205
259
|
{
|
|
206
260
|
role: "zeta.advisor",
|
|
207
|
-
tone:
|
|
208
|
-
head,
|
|
261
|
+
tone: look.tone,
|
|
209
262
|
collapsible: notes.length > COLLAPSED_NOTES,
|
|
210
263
|
collapsed: notes.length > COLLAPSED_NOTES ? !isExpanded : undefined,
|
|
211
264
|
preview: notes.length > COLLAPSED_NOTES ? "auto" : undefined,
|
|
212
265
|
},
|
|
213
|
-
body,
|
|
266
|
+
[head, ...body],
|
|
214
267
|
);
|
|
215
268
|
};
|
|
216
269
|
|
|
@@ -231,7 +284,7 @@ export function createAdvisorMessageCard(
|
|
|
231
284
|
},
|
|
232
285
|
describe(): NativeNode {
|
|
233
286
|
const isExpanded = expanded();
|
|
234
|
-
return nativeMemo.get([isExpanded
|
|
287
|
+
return nativeMemo.get([isExpanded], () => describeCard(isExpanded));
|
|
235
288
|
},
|
|
236
289
|
setExpanded(value: boolean): void {
|
|
237
290
|
lastGlobal = value;
|
|
@@ -2,7 +2,7 @@ import type { AssistantMessage, ImageContent } from "@linxiraos/pi-ai";
|
|
|
2
2
|
import { type Component, Container } from "../tui";
|
|
3
3
|
import { type ImageBudget } from "../components/image";
|
|
4
4
|
import type { AssistantThinkingRenderer } from "./extension-types";
|
|
5
|
-
import type { NativeNode, NativeUiEvent } from "../native/node";
|
|
5
|
+
import type { DescribeContext, NativeNode, NativeUiEvent } from "../native/node";
|
|
6
6
|
import { type CacheInvalidation } from "./cache-invalidation-marker";
|
|
7
7
|
import { type ServedModelMismatch } from "./served-model-marker";
|
|
8
8
|
import { type TranscriptStableRow } from "../chrome/transcript-container";
|
|
@@ -99,13 +99,13 @@ export declare class AssistantMessageComponent extends Container {
|
|
|
99
99
|
* head's `title`. With `hideThinkingBlock` only the live head shows (collapsed)
|
|
100
100
|
* and a finished thought emits nothing.
|
|
101
101
|
*/
|
|
102
|
-
describe(): NativeNode;
|
|
102
|
+
describe(cx?: DescribeContext): NativeNode;
|
|
103
103
|
/**
|
|
104
104
|
* A TTSR rule rewound this partial answer: a native terminal dims it and
|
|
105
105
|
* tags it `↺ rewound` (the ANSI render leaves it as it stopped).
|
|
106
106
|
*/
|
|
107
107
|
markRewound(): void;
|
|
108
|
-
/** Mirrors a thinking section collapsed or expanded in the terminal. */
|
|
108
|
+
/** Mirrors a thinking section collapsed or expanded in the terminal; runs its controls' clicks. */
|
|
109
109
|
handleNativeEvent(event: NativeUiEvent): void;
|
|
110
110
|
/** Width-independent stable identities for the streamed leading thinking run. */
|
|
111
111
|
getTranscriptStableRows(): readonly TranscriptStableRow[];
|
|
@@ -130,7 +130,12 @@ export declare class AssistantMessageComponent extends Container {
|
|
|
130
130
|
setTableChartsVisible(visible: boolean): void;
|
|
131
131
|
/** Toggle only images produced by tool results; assistant-native images remain governed by setImagesVisible. */
|
|
132
132
|
setToolResultImagesVisible(visible: boolean): void;
|
|
133
|
-
|
|
133
|
+
/**
|
|
134
|
+
* A grouped read's result images, and `mediaPath`: the file it read that Tern's media viewer
|
|
135
|
+
* shows (read details), which a native host supporting `media` shows by path instead of the
|
|
136
|
+
* images. A media-only result (a model, a PDF) passes no images.
|
|
137
|
+
*/
|
|
138
|
+
setToolResultImages(toolCallId: string, images: ImageContent[], mediaPath?: string): void;
|
|
134
139
|
updateContent(message: AssistantMessage, opts?: {
|
|
135
140
|
transient?: boolean;
|
|
136
141
|
}): void;
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import type { AssistantMessage, ImageContent, TextContent } from "@linxiraos/pi-ai";
|
|
2
|
+
import { presentNarrationAsAnswer } from "@linxiraos/pi-ai/utils/narration";
|
|
2
3
|
import { type Component, Container } from "../tui";
|
|
3
4
|
import { Image, type ImageBudget } from "../components/image";
|
|
4
5
|
import { ImageProtocol, TERMINAL } from "../terminal-capabilities";
|
|
5
6
|
import {
|
|
6
7
|
type DefaultTextStyle,
|
|
8
|
+
holdPartialTableRow,
|
|
7
9
|
Markdown,
|
|
8
10
|
type MarkdownTheme,
|
|
9
11
|
rewriteMarkdownLinkDestinations,
|
|
12
|
+
speculativeStreamingSource,
|
|
13
|
+
streamingMarkdownSource,
|
|
10
14
|
} from "../components/markdown";
|
|
11
15
|
import { Spacer } from "../components/spacer";
|
|
12
16
|
import { Text } from "../components/text";
|
|
@@ -17,9 +21,10 @@ import type { AssistantThinkingRenderer } from "./extension-types";
|
|
|
17
21
|
import { ensureThemeSync, getMarkdownTheme, getMarkdownThemeWithLinkTargets, getThemeEpoch, theme } from "../theme";
|
|
18
22
|
import { card, col, elapsed, node, span, text } from "../native/describe";
|
|
19
23
|
import { hasTranscriptActions, runTranscriptAction } from "./transcript-actions";
|
|
20
|
-
import type { NativeChild, NativeNode, NativeUiEvent } from "../native/node";
|
|
24
|
+
import type { DescribeContext, NativeChild, NativeNode, NativeUiEvent } from "../native/node";
|
|
21
25
|
import { isNativeRendering } from "../native/state";
|
|
22
|
-
import { NativeImageCache } from "../native/blobs";
|
|
26
|
+
import { NativeImageCache, nativeMediaNode } from "../native/blobs";
|
|
27
|
+
import { linkedImageText } from "../prompt/image-references";
|
|
23
28
|
import { Memo } from "../native/memo";
|
|
24
29
|
import { EMPTY_LINK_TARGETS, resolveImageOptions } from "../render/render-utils";
|
|
25
30
|
import { WidthAwareText } from "../render";
|
|
@@ -35,7 +40,7 @@ import { formatTurnUsage, type TurnUsageSummary } from "../overlays/usage-row";
|
|
|
35
40
|
import { FigureMarkdown } from "./figure-markdown";
|
|
36
41
|
import { svgFigureRendering } from "./svg-figure";
|
|
37
42
|
import { hasSvgFence } from "./svg-source";
|
|
38
|
-
import {
|
|
43
|
+
import { chartFence, hasChartTable, lookupTableChart, splitTableCharts } from "./table-chart";
|
|
39
44
|
|
|
40
45
|
/**
|
|
41
46
|
* Max wrapped rows of a turn-ending provider error rendered inline in the
|
|
@@ -111,6 +116,14 @@ interface StableRenderInputs {
|
|
|
111
116
|
readonly color: ((text: string) => string) | undefined;
|
|
112
117
|
}
|
|
113
118
|
|
|
119
|
+
/**
|
|
120
|
+
* A partial last thinking line the prose fold may still drop: the start of a
|
|
121
|
+
* fence marker (`` ` ``, `~`) or of an empty `<!-- -->` sentinel. Once it turns
|
|
122
|
+
* into either, the fold's ellipsis lands back on the line above it
|
|
123
|
+
* (`formatThinkingForDisplay`), so that line is not yet settled.
|
|
124
|
+
*/
|
|
125
|
+
const UNSETTLED_THINKING_TAIL = /(?:^|\n)[^\S\n]*(?:`{1,2}|~{1,2}|<(?:!(?:-(?:-[^\S\n]*-{0,2})?)?)?)[^\S\n]*$/;
|
|
126
|
+
|
|
114
127
|
function isSnapshotExtension(previous: readonly StablePart[], current: readonly StablePart[]): boolean {
|
|
115
128
|
if (previous.length > current.length) return false;
|
|
116
129
|
for (let index = 0; index < previous.length; index++) {
|
|
@@ -250,6 +263,8 @@ export class AssistantMessageComponent extends Container {
|
|
|
250
263
|
#lastMessage?: AssistantMessage;
|
|
251
264
|
#emergencyText?: ProseBlock;
|
|
252
265
|
#toolImagesByCallId = new Map<string, ImageContent[]>();
|
|
266
|
+
/** File each grouped read showed that Tern's media viewer shows, for native `media` nodes. */
|
|
267
|
+
#toolMediaPathByCallId = new Map<string, string>();
|
|
253
268
|
/**
|
|
254
269
|
* Payload keys ({@link imagePayloadKey}) whose Kitty PNG conversion this
|
|
255
270
|
* component is awaiting, so a re-delivered image neither re-encodes nor
|
|
@@ -798,7 +813,8 @@ export class AssistantMessageComponent extends Container {
|
|
|
798
813
|
* head's `title`. With `hideThinkingBlock` only the live head shows (collapsed)
|
|
799
814
|
* and a finished thought emits nothing.
|
|
800
815
|
*/
|
|
801
|
-
override describe(): NativeNode {
|
|
816
|
+
override describe(cx?: DescribeContext): NativeNode {
|
|
817
|
+
const media = cx?.supports("media") === true;
|
|
802
818
|
const tail = this.#displayedMessage ? this.#thinkingTailIndex(this.#displayedMessage) : undefined;
|
|
803
819
|
const rate =
|
|
804
820
|
tail !== undefined && this.#lastUpdateTransient && this.#thinkingRateLive
|
|
@@ -813,9 +829,10 @@ export class AssistantMessageComponent extends Container {
|
|
|
813
829
|
this.#errorExpanded,
|
|
814
830
|
this.#nativeViewVersion,
|
|
815
831
|
rate,
|
|
832
|
+
media,
|
|
816
833
|
getThemeEpoch(),
|
|
817
834
|
];
|
|
818
|
-
return this.#native.get(key, () => this.#describeMessage(tail, rate));
|
|
835
|
+
return this.#native.get(key, () => this.#describeMessage(tail, rate, media));
|
|
819
836
|
}
|
|
820
837
|
|
|
821
838
|
/**
|
|
@@ -828,10 +845,10 @@ export class AssistantMessageComponent extends Container {
|
|
|
828
845
|
this.#nativeViewVersion++;
|
|
829
846
|
}
|
|
830
847
|
|
|
831
|
-
/** Mirrors a thinking section collapsed or expanded in the terminal. */
|
|
848
|
+
/** Mirrors a thinking section collapsed or expanded in the terminal; runs its controls' clicks. */
|
|
832
849
|
handleNativeEvent(event: NativeUiEvent): void {
|
|
833
850
|
if (event.type === "action") {
|
|
834
|
-
this.#
|
|
851
|
+
this.#handleAction(event.act);
|
|
835
852
|
return;
|
|
836
853
|
}
|
|
837
854
|
if (event.type !== "toggle" || !event.key.startsWith("k")) return;
|
|
@@ -841,7 +858,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
841
858
|
this.#nativeViewVersion++;
|
|
842
859
|
}
|
|
843
860
|
|
|
844
|
-
#describeMessage(thinkingTail: number | undefined, rate: number): NativeNode {
|
|
861
|
+
#describeMessage(thinkingTail: number | undefined, rate: number, media: boolean): NativeNode {
|
|
845
862
|
const message = this.#displayedMessage;
|
|
846
863
|
const previousParts = this.#nativeParts;
|
|
847
864
|
const parts = new Map<string, { text: string; stream: boolean; node: NativeNode }>();
|
|
@@ -854,12 +871,19 @@ export class AssistantMessageComponent extends Container {
|
|
|
854
871
|
: {
|
|
855
872
|
text: source,
|
|
856
873
|
stream,
|
|
857
|
-
|
|
874
|
+
// A streaming node renders its open link or math span as already closed, as the TUI does.
|
|
875
|
+
node: node(
|
|
876
|
+
"md",
|
|
877
|
+
stream ? { text: speculativeStreamingSource(source), stream: true } : { text: source },
|
|
878
|
+
undefined,
|
|
879
|
+
key,
|
|
880
|
+
),
|
|
858
881
|
};
|
|
859
882
|
parts.set(slot, entry);
|
|
860
883
|
return entry.node;
|
|
861
884
|
};
|
|
862
885
|
const children: NativeChild[] = [];
|
|
886
|
+
let answered = false;
|
|
863
887
|
if (message) {
|
|
864
888
|
const live = !this.#transcriptBlockFinalized && this.#lastUpdateTransient;
|
|
865
889
|
let tailIndex = -1;
|
|
@@ -876,7 +900,10 @@ export class AssistantMessageComponent extends Container {
|
|
|
876
900
|
const content = message.content[index]!;
|
|
877
901
|
const streaming = live && index === tailIndex;
|
|
878
902
|
if (content.type === "text" && canonicalizeMessage(content.text)) {
|
|
879
|
-
|
|
903
|
+
answered = true;
|
|
904
|
+
// A native terminal sizes table columns by every row it gets: send a growing row once
|
|
905
|
+
// its line ends. Hold before trimming, which would drop the newline ending a finished row.
|
|
906
|
+
const source = (streaming ? holdPartialTableRow(content.text) : content.text).trim();
|
|
880
907
|
// The terminal renders this source itself and would resolve a relative
|
|
881
908
|
// link against its own idea of the cwd: hand it the session-resolved
|
|
882
909
|
// targets (resolved once the segment closes, so never while streaming).
|
|
@@ -893,7 +920,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
893
920
|
children.push(markdown(`t${index}`, `t${index}`, linked(source, streaming), streaming));
|
|
894
921
|
continue;
|
|
895
922
|
}
|
|
896
|
-
// A chart follows its table as an
|
|
923
|
+
// A chart follows its table as an ```svg figure the terminal colors; the prose splits around it.
|
|
897
924
|
const segments = splitTableCharts(source, streaming);
|
|
898
925
|
segments.forEach((segment, part) => {
|
|
899
926
|
const slot = part === 0 ? `t${index}` : `t${index}.${part}`;
|
|
@@ -904,7 +931,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
904
931
|
}
|
|
905
932
|
const chart = lookupTableChart(segment.table);
|
|
906
933
|
if (chart instanceof Promise) this.#awaitChart(chart);
|
|
907
|
-
else if (chart) children.push(
|
|
934
|
+
else if (chart) children.push(markdown(slot, slot, chartFence(chart), false));
|
|
908
935
|
});
|
|
909
936
|
} else if (content.type === "thinking") {
|
|
910
937
|
const display = resolveThinkingDisplay(content, this.#proseOnlyThinking);
|
|
@@ -960,9 +987,26 @@ export class AssistantMessageComponent extends Container {
|
|
|
960
987
|
}
|
|
961
988
|
}
|
|
962
989
|
if (this.#showImages && this.#showToolResultImages) {
|
|
990
|
+
// Reads of files Tern's media viewer shows: the host loads each file once by path, no bytes uploaded.
|
|
991
|
+
const shownPaths = new Set<string>();
|
|
992
|
+
if (media) {
|
|
993
|
+
for (const [toolCallId, mediaPath] of this.#toolMediaPathByCallId) {
|
|
994
|
+
if (shownPaths.has(mediaPath)) continue;
|
|
995
|
+
shownPaths.add(mediaPath);
|
|
996
|
+
children.push(nativeMediaNode(mediaPath, `r${toolCallId}`));
|
|
997
|
+
}
|
|
998
|
+
}
|
|
963
999
|
for (const [toolCallId, images] of this.#toolImagesByCallId) {
|
|
1000
|
+
if (media && this.#toolMediaPathByCallId.has(toolCallId)) continue;
|
|
964
1001
|
images.forEach((image, index) => {
|
|
965
|
-
children.push(
|
|
1002
|
+
children.push(
|
|
1003
|
+
this.#nativeImages.get(
|
|
1004
|
+
`r${toolCallId}:${index}`,
|
|
1005
|
+
image.data,
|
|
1006
|
+
image.mimeType,
|
|
1007
|
+
this.#toolMediaPathByCallId.get(toolCallId),
|
|
1008
|
+
),
|
|
1009
|
+
);
|
|
966
1010
|
});
|
|
967
1011
|
}
|
|
968
1012
|
}
|
|
@@ -983,14 +1027,32 @@ export class AssistantMessageComponent extends Container {
|
|
|
983
1027
|
}
|
|
984
1028
|
if (this.#turnUsage) {
|
|
985
1029
|
const usage = formatTurnUsage(this.#turnUsage);
|
|
1030
|
+
const line: NativeChild[] = [];
|
|
1031
|
+
// Led by Copy and a dot: `⧉ · 12.4s · 19K tok this turn`.
|
|
1032
|
+
if (answered && hasTranscriptActions()) {
|
|
1033
|
+
line.push(
|
|
1034
|
+
// `copy-answer`, not Tern's local `copy` (that would copy the icon's empty label).
|
|
1035
|
+
node(
|
|
1036
|
+
"icon",
|
|
1037
|
+
{
|
|
1038
|
+
name: "copy",
|
|
1039
|
+
role: "zeta.turn.copy",
|
|
1040
|
+
title: "Copy answer",
|
|
1041
|
+
aria: "Copy answer",
|
|
1042
|
+
actions: { click: "copy-answer" },
|
|
1043
|
+
},
|
|
1044
|
+
undefined,
|
|
1045
|
+
"copy",
|
|
1046
|
+
),
|
|
1047
|
+
node("text", { text: "·" }, undefined, "dot"),
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
line.push(node("text", { text: usage.text }, undefined, "text"));
|
|
986
1051
|
children.push(
|
|
987
1052
|
node(
|
|
988
1053
|
"row",
|
|
989
1054
|
{ role: "zeta.turn.usage", gap: "xs", align: "center", title: usage.title },
|
|
990
|
-
|
|
991
|
-
node("icon", { name: "time" }, undefined, "icon"),
|
|
992
|
-
node("text", { text: usage.text }, undefined, "text"),
|
|
993
|
-
],
|
|
1055
|
+
line,
|
|
994
1056
|
"turn-usage",
|
|
995
1057
|
),
|
|
996
1058
|
);
|
|
@@ -1081,11 +1143,24 @@ export class AssistantMessageComponent extends Container {
|
|
|
1081
1143
|
return card({ role: "zeta.error", tone: "error", key: "error" }, children);
|
|
1082
1144
|
}
|
|
1083
1145
|
|
|
1084
|
-
/**
|
|
1085
|
-
#
|
|
1146
|
+
/** Control clicks (the error frame's actions, the turn footer's Copy): zeta's own retry, clipboard and model-picker paths. */
|
|
1147
|
+
#handleAction(act: string): void {
|
|
1086
1148
|
if (act === "retry") runTranscriptAction({ act: "retry" });
|
|
1087
1149
|
else if (act === "switch-model") runTranscriptAction({ act: "switch-model" });
|
|
1088
1150
|
else if (act === "copy-error" && this.#errorText) runTranscriptAction({ act: "copy", text: this.#errorText });
|
|
1151
|
+
else if (act === "copy-answer") {
|
|
1152
|
+
const text = this.#answerText();
|
|
1153
|
+
if (text) runTranscriptAction({ act: "copy", text });
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/** The answer as the transcript shows it: its text blocks' markdown, a blank line apart, without thinking or tool calls. */
|
|
1158
|
+
#answerText(): string {
|
|
1159
|
+
const blocks: string[] = [];
|
|
1160
|
+
for (const content of this.#displayedMessage?.content ?? []) {
|
|
1161
|
+
if (content.type === "text" && canonicalizeMessage(content.text)) blocks.push(content.text.trim());
|
|
1162
|
+
}
|
|
1163
|
+
return blocks.join("\n\n");
|
|
1089
1164
|
}
|
|
1090
1165
|
|
|
1091
1166
|
/** Width-independent stable identities for the streamed leading thinking run. */
|
|
@@ -1198,17 +1273,23 @@ export class AssistantMessageComponent extends Container {
|
|
|
1198
1273
|
// Streaming child: publish Markdown's frozen prefix, and only
|
|
1199
1274
|
// once non-blank content exists past it — the block's last
|
|
1200
1275
|
// non-blank line is still being written (thinking's prose fold
|
|
1201
|
-
// may rewrite it) and must stay out of published bytes.
|
|
1276
|
+
// may rewrite it) and must stay out of published bytes. A
|
|
1277
|
+
// partial thinking line the fold may still drop does not count.
|
|
1278
|
+
// The frozen prefix is in streamingMarkdownSource form, so
|
|
1279
|
+
// offsets into the block use that form too.
|
|
1202
1280
|
const raw = md.getLastRenderStableText();
|
|
1203
1281
|
const frozen = raw.trim();
|
|
1204
|
-
|
|
1282
|
+
let past = streamingMarkdownSource(item.lastText).slice(raw.length);
|
|
1283
|
+
if (item.blockType === "thinking") past = past.replace(UNSETTLED_THINKING_TAIL, "");
|
|
1284
|
+
if (frozen.length > 0 && /\S/.test(past)) {
|
|
1205
1285
|
parts.push({ kind: item.blockType, text: frozen });
|
|
1206
1286
|
}
|
|
1207
1287
|
break;
|
|
1208
1288
|
}
|
|
1209
1289
|
// Closed child: only the streaming tail may mutate in place, so
|
|
1210
|
-
// everything before it is final.
|
|
1211
|
-
|
|
1290
|
+
// everything before it is final. Same form as the frozen prefix it
|
|
1291
|
+
// published while streaming, so the snapshot still extends.
|
|
1292
|
+
parts.push({ kind: item.blockType, text: streamingMarkdownSource(item.lastText) });
|
|
1212
1293
|
itemIndex++;
|
|
1213
1294
|
continue;
|
|
1214
1295
|
}
|
|
@@ -1493,8 +1574,15 @@ export class AssistantMessageComponent extends Container {
|
|
|
1493
1574
|
}
|
|
1494
1575
|
}
|
|
1495
1576
|
|
|
1496
|
-
|
|
1577
|
+
/**
|
|
1578
|
+
* A grouped read's result images, and `mediaPath`: the file it read that Tern's media viewer
|
|
1579
|
+
* shows (read details), which a native host supporting `media` shows by path instead of the
|
|
1580
|
+
* images. A media-only result (a model, a PDF) passes no images.
|
|
1581
|
+
*/
|
|
1582
|
+
setToolResultImages(toolCallId: string, images: ImageContent[], mediaPath?: string): void {
|
|
1497
1583
|
if (!toolCallId) return;
|
|
1584
|
+
if (mediaPath) this.#toolMediaPathByCallId.set(toolCallId, mediaPath);
|
|
1585
|
+
else this.#toolMediaPathByCallId.delete(toolCallId);
|
|
1498
1586
|
const validImages = images.filter(img => img.type === "image" && img.data && img.mimeType);
|
|
1499
1587
|
if (validImages.length === 0) {
|
|
1500
1588
|
this.#toolImagesByCallId.delete(toolCallId);
|
|
@@ -1536,12 +1624,15 @@ export class AssistantMessageComponent extends Container {
|
|
|
1536
1624
|
}
|
|
1537
1625
|
}
|
|
1538
1626
|
|
|
1539
|
-
#renderImageEntries(
|
|
1627
|
+
#renderImageEntries(
|
|
1628
|
+
entries: Array<{ image: ImageContent; key: string; sourcePath?: string }>,
|
|
1629
|
+
withLeadingSpacer: boolean,
|
|
1630
|
+
): void {
|
|
1540
1631
|
if (!this.#showImages || entries.length === 0) return;
|
|
1541
1632
|
this.#convertImagesForKitty(entries);
|
|
1542
1633
|
|
|
1543
1634
|
if (withLeadingSpacer) this.#contentContainer.addChild(new Spacer(1));
|
|
1544
|
-
for (const { image, key } of entries) {
|
|
1635
|
+
for (const { image, key, sourcePath } of entries) {
|
|
1545
1636
|
let displayImage: ImageContent | undefined = image;
|
|
1546
1637
|
if (TERMINAL.imageProtocol === ImageProtocol.Kitty && image.mimeType !== "image/png") {
|
|
1547
1638
|
displayImage = this.#kittyConversion(image);
|
|
@@ -1555,6 +1646,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
1555
1646
|
{ fallbackColor: (text: string) => theme.fg("toolOutput", text) },
|
|
1556
1647
|
{
|
|
1557
1648
|
...resolveImageOptions(),
|
|
1649
|
+
sourcePath,
|
|
1558
1650
|
budget: this.#imageBudget,
|
|
1559
1651
|
imageKey: key,
|
|
1560
1652
|
requestRender: this.#onImageUpdate,
|
|
@@ -1563,15 +1655,18 @@ export class AssistantMessageComponent extends Container {
|
|
|
1563
1655
|
);
|
|
1564
1656
|
continue;
|
|
1565
1657
|
}
|
|
1566
|
-
this.#contentContainer.addChild(
|
|
1658
|
+
this.#contentContainer.addChild(
|
|
1659
|
+
new Text(theme.fg("toolOutput", linkedImageText(`[Image: ${image.mimeType}]`, image, sourcePath)), 1, 0),
|
|
1660
|
+
);
|
|
1567
1661
|
}
|
|
1568
1662
|
}
|
|
1569
1663
|
|
|
1570
1664
|
#renderToolImages(): void {
|
|
1571
1665
|
if (!this.#showToolResultImages) return;
|
|
1572
|
-
const entries = Array.from(this.#toolImagesByCallId.entries()).flatMap(([toolCallId, images]) =>
|
|
1573
|
-
|
|
1574
|
-
|
|
1666
|
+
const entries = Array.from(this.#toolImagesByCallId.entries()).flatMap(([toolCallId, images]) => {
|
|
1667
|
+
const sourcePath = this.#toolMediaPathByCallId.get(toolCallId);
|
|
1668
|
+
return images.map((image, index) => ({ image, key: `${toolCallId}:${index}`, sourcePath }));
|
|
1669
|
+
});
|
|
1575
1670
|
this.#renderImageEntries(entries, true);
|
|
1576
1671
|
}
|
|
1577
1672
|
|
|
@@ -1714,7 +1809,8 @@ export class AssistantMessageComponent extends Container {
|
|
|
1714
1809
|
this.#lastUpdateTransient = opts?.transient === true;
|
|
1715
1810
|
// Everything below renders the display form; #lastMessage keeps the
|
|
1716
1811
|
// verbatim message so re-renders re-derive the reaction deterministically.
|
|
1717
|
-
|
|
1812
|
+
// Narration the provider returned as thinking renders as answer text.
|
|
1813
|
+
message = this.#displayMessage(presentNarrationAsAnswer(message), this.#lastUpdateTransient);
|
|
1718
1814
|
this.#displayedMessage = message;
|
|
1719
1815
|
|
|
1720
1816
|
// Streaming-speed gauge: only a live, in-flight render of the single
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
import { span, text } from "../native/describe";
|
|
32
32
|
import { type DescribeContext, type NativeNode, type NativeUiEvent, rootToggleExpanded } from "../native/node";
|
|
33
33
|
import { NativeImageCache } from "../native/blobs";
|
|
34
|
+
import { imageLinksEnabled, linkedImageSpan, linkedImageText } from "../prompt/image-references";
|
|
34
35
|
import { Memo } from "../native/memo";
|
|
35
36
|
|
|
36
37
|
const STREAMING_LINE_CAP = PREVIEW_LINES * 5;
|
|
@@ -172,14 +173,18 @@ export class BashExecutionComponent extends Container {
|
|
|
172
173
|
this.#expanded,
|
|
173
174
|
this.#showImages,
|
|
174
175
|
this.#images.length,
|
|
176
|
+
// Image nodes and placeholders carry links only under an enabled policy.
|
|
177
|
+
imageLinksEnabled(),
|
|
175
178
|
];
|
|
176
179
|
return this.#native.get(key, () => {
|
|
177
180
|
const images = this.#images.map((image, index) =>
|
|
178
181
|
this.#showImages
|
|
179
182
|
? this.#nativeImages.get(`img${index}`, image.data, image.mimeType)
|
|
180
183
|
: text([
|
|
181
|
-
|
|
184
|
+
linkedImageSpan(
|
|
182
185
|
imageFallback(image.mimeType, getImageDimensions(image.data, image.mimeType) ?? undefined),
|
|
186
|
+
image,
|
|
187
|
+
undefined,
|
|
183
188
|
"muted",
|
|
184
189
|
),
|
|
185
190
|
]),
|
|
@@ -418,7 +423,7 @@ export class BashExecutionComponent extends Container {
|
|
|
418
423
|
} else {
|
|
419
424
|
const dimensions = getImageDimensions(image.data, image.mimeType) ?? undefined;
|
|
420
425
|
this.#contentContainer.addChild(
|
|
421
|
-
new Text(theme.fg("muted", imageFallback(image.mimeType, dimensions)), 1, 0),
|
|
426
|
+
new Text(theme.fg("muted", linkedImageText(imageFallback(image.mimeType, dimensions), image)), 1, 0),
|
|
422
427
|
);
|
|
423
428
|
}
|
|
424
429
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ImageBudget } from "../components/image";
|
|
2
|
-
import type
|
|
2
|
+
import { type Markdown } from "../components/markdown";
|
|
3
3
|
import { Container } from "../tui";
|
|
4
4
|
export interface FigureMarkdownOptions {
|
|
5
5
|
/** Markdown for one prose run (and a failed figure's code fallback); FigureMarkdown drives its transience. */
|
|
@@ -32,7 +32,12 @@ export declare class FigureMarkdown extends Container {
|
|
|
32
32
|
/**
|
|
33
33
|
* The trimmed prose ahead of the first figure, or "" when the block opens
|
|
34
34
|
* with one. These are the only rows plain Markdown reproduces byte for
|
|
35
|
-
* byte, so they are all a transcript may publish ahead of the figure.
|
|
35
|
+
* byte, so they are all a transcript may publish ahead of the figure. While
|
|
36
|
+
* that prose is still the streaming tail (no figure follows it yet), only
|
|
37
|
+
* its settled prefix ({@link Markdown.getLastRenderStableText}) qualifies:
|
|
38
|
+
* the rest is still being written. Both forms are in
|
|
39
|
+
* {@link streamingMarkdownSource} form, so the settled prefix stays a byte
|
|
40
|
+
* prefix of the whole run once a figure splits it off.
|
|
36
41
|
*/
|
|
37
42
|
get leadingProse(): string;
|
|
38
43
|
/** Whether a figure's final source still waits for its raster, or a chart for its pick or raster. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Tokens } from "@linxiraos/pi-utils/marked";
|
|
2
2
|
import type { ImageBudget } from "../components/image";
|
|
3
|
-
import type
|
|
3
|
+
import { type Markdown, streamingMarkdownSource } from "../components/markdown";
|
|
4
4
|
import { Spacer } from "../components/spacer";
|
|
5
5
|
import { Container } from "../tui";
|
|
6
6
|
import { SvgFigure, svgFigureRendering } from "./svg-figure";
|
|
@@ -68,11 +68,18 @@ export class FigureMarkdown extends Container {
|
|
|
68
68
|
/**
|
|
69
69
|
* The trimmed prose ahead of the first figure, or "" when the block opens
|
|
70
70
|
* with one. These are the only rows plain Markdown reproduces byte for
|
|
71
|
-
* byte, so they are all a transcript may publish ahead of the figure.
|
|
71
|
+
* byte, so they are all a transcript may publish ahead of the figure. While
|
|
72
|
+
* that prose is still the streaming tail (no figure follows it yet), only
|
|
73
|
+
* its settled prefix ({@link Markdown.getLastRenderStableText}) qualifies:
|
|
74
|
+
* the rest is still being written. Both forms are in
|
|
75
|
+
* {@link streamingMarkdownSource} form, so the settled prefix stays a byte
|
|
76
|
+
* prefix of the whole run once a figure splits it off.
|
|
72
77
|
*/
|
|
73
78
|
get leadingProse(): string {
|
|
74
79
|
const first = this.#parts[0];
|
|
75
|
-
|
|
80
|
+
if (first?.kind !== "markdown") return "";
|
|
81
|
+
if (this.#transient && this.#parts.length === 1) return first.md.getLastRenderStableText().trim();
|
|
82
|
+
return streamingMarkdownSource(first.text);
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
/** Whether a figure's final source still waits for its raster, or a chart for its pick or raster. */
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { ImageContent, Model } from "@linxiraos/pi-ai";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
decodeBase64ImageData,
|
|
4
|
+
formatBytes,
|
|
5
|
+
parseImageMetadata,
|
|
6
|
+
SUPPORTED_IMAGE_MIME_TYPES,
|
|
7
|
+
} from "@linxiraos/pi-utils";
|
|
3
8
|
|
|
4
9
|
export const MAX_IMAGE_INPUT_BYTES = 20 * 1024 * 1024;
|
|
5
10
|
|
|
@@ -83,11 +88,9 @@ const DECODE_PROBE_EDGE_PX = 1;
|
|
|
83
88
|
* truth on both sides. Callers on hot paths must cache the verdict.
|
|
84
89
|
*/
|
|
85
90
|
export async function imageDecodeFailureReason(image: ImageContent): Promise<string | null> {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const bytes =
|
|
89
|
-
if (bytes.length === 0) return "empty image data";
|
|
90
|
-
if (bytes.toString("base64").replace(/=+$/, "") !== normalizedData) return "invalid base64 image data";
|
|
91
|
+
const decoded = decodeBase64ImageData(image.data);
|
|
92
|
+
if (decoded.reason !== undefined) return decoded.reason;
|
|
93
|
+
const { bytes } = decoded;
|
|
91
94
|
const detected = parseImageMetadata(bytes);
|
|
92
95
|
if (detected && detected.mimeType !== image.mimeType.toLowerCase()) {
|
|
93
96
|
return `declared ${image.mimeType} but contains ${detected.mimeType}`;
|