@oh-my-pi/pi-tui 18.13.2 → 18.14.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 +40 -54
- package/README.md +34 -34
- package/dist/types/app-keybindings.d.ts +15 -0
- package/dist/types/apps/session-picker.d.ts +5 -0
- package/dist/types/charts/chart-plan.d.ts +9 -7
- package/dist/types/chat/assistant-message.d.ts +15 -3
- package/dist/types/chat/bash-execution.d.ts +2 -0
- package/dist/types/chat/display-preferences.d.ts +2 -0
- package/dist/types/chat/extension-types.d.ts +31 -0
- package/dist/types/chat/reply-write.d.ts +48 -0
- package/dist/types/chat/table-chart.d.ts +4 -3
- package/dist/types/chat/tool-execution.d.ts +1 -2
- package/dist/types/chat/transcript-outline.d.ts +7 -0
- package/dist/types/chat/transcript-render-helpers.d.ts +2 -0
- package/dist/types/chrome/index.d.ts +1 -0
- package/dist/types/chrome/transcript-notice.d.ts +35 -0
- package/dist/types/chrome/transcript-status.d.ts +20 -8
- package/dist/types/components/editor.d.ts +8 -0
- package/dist/types/components/markdown.d.ts +7 -0
- package/dist/types/native/blobs.d.ts +2 -1
- package/dist/types/native/picker.d.ts +3 -3
- package/dist/types/overlays/annotation-overlay.d.ts +7 -1
- package/dist/types/overlays/annotation-types.d.ts +28 -0
- package/dist/types/overlays/login-dialog.d.ts +5 -1
- package/dist/types/overlays/model-browser.d.ts +10 -1
- package/dist/types/overlays/oauth-selector.d.ts +23 -0
- package/dist/types/overlays/plan-review-overlay.d.ts +2 -0
- package/dist/types/overlays/rewind-selector.d.ts +1 -1
- package/dist/types/overlays/session-selector.d.ts +22 -2
- package/dist/types/prompt/composer.d.ts +41 -4
- package/dist/types/prompt/interactive-context-helpers.d.ts +10 -3
- package/dist/types/prompt/welcome.d.ts +6 -2
- package/dist/types/render/extension-links.d.ts +65 -0
- package/dist/types/render/hyperlink.d.ts +25 -7
- package/dist/types/render/index.d.ts +1 -0
- package/dist/types/setup/scenes/model.d.ts +1 -1
- package/dist/types/setup/scenes/outro.d.ts +5 -4
- package/dist/types/setup/scenes/sign-in.d.ts +13 -8
- package/dist/types/setup/scenes/splash.d.ts +4 -4
- package/dist/types/setup/scenes/types.d.ts +16 -1
- package/dist/types/setup/startup-splash.d.ts +1 -1
- package/dist/types/setup/wizard-overlay.d.ts +11 -7
- package/dist/types/setup/wizard.d.ts +5 -1
- package/dist/types/status-line/component.d.ts +1 -1
- package/dist/types/status-line/context-usage.d.ts +28 -6
- package/dist/types/status-line/host.d.ts +6 -2
- package/dist/types/theme/theme.d.ts +0 -23
- package/dist/types/tools/bash-interactive.d.ts +6 -0
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/default-renderer.d.ts +6 -4
- package/dist/types/tools/irc.d.ts +6 -0
- package/dist/types/tools/json-tree.d.ts +23 -4
- package/dist/types/tools/lsp.d.ts +2 -0
- package/dist/types/tools/mcp.d.ts +6 -5
- package/dist/types/tools/native-view.d.ts +1 -1
- package/dist/types/tools/renderer.d.ts +2 -0
- package/dist/types/tools/streaming-output.d.ts +11 -0
- package/dist/types/tools/task.d.ts +9 -1
- package/dist/types/tools/url-scheme-host.d.ts +17 -0
- package/dist/types/tools/yield.d.ts +1 -1
- package/dist/types/tui.d.ts +7 -2
- package/package.json +10 -15
- package/src/app-keybindings.ts +15 -0
- package/src/apps/session-picker.ts +7 -0
- package/src/charts/chart-plan.ts +20 -50
- package/src/chat/advisor-message.ts +2 -0
- package/src/chat/assistant-message.ts +137 -19
- package/src/chat/background-tan-message.ts +4 -0
- package/src/chat/bash-execution.ts +6 -0
- package/src/chat/chat-transcript-builder.ts +58 -28
- package/src/chat/compaction-summary-message.ts +2 -1
- package/src/chat/display-preferences.ts +3 -0
- package/src/chat/extension-types.ts +34 -0
- package/src/chat/reply-write.ts +127 -0
- package/src/chat/table-chart.ts +10 -4
- package/src/chat/tool-execution.ts +20 -13
- package/src/chat/transcript-outline.ts +27 -0
- package/src/chat/transcript-render-helpers.ts +66 -24
- package/src/chrome/index.ts +1 -0
- package/src/chrome/transcript-notice.ts +83 -0
- package/src/chrome/transcript-status.ts +29 -29
- package/src/components/editor.ts +10 -1
- package/src/components/markdown.ts +116 -6
- package/src/hotkeys-markdown.ts +1 -0
- package/src/native/backend.ts +5 -66
- package/src/native/blobs.ts +3 -2
- package/src/native/picker.ts +3 -3
- package/src/overlays/advisor-config.ts +1 -10
- package/src/overlays/annotation-overlay.ts +125 -37
- package/src/overlays/annotation-types.ts +24 -0
- package/src/overlays/hook-selector.ts +2 -2
- package/src/overlays/login-dialog.ts +31 -5
- package/src/overlays/model-browser.ts +37 -0
- package/src/overlays/model-hub.ts +1 -7
- package/src/overlays/model-picker.ts +1 -10
- package/src/overlays/oauth-selector.ts +44 -6
- package/src/overlays/plan-review-overlay.ts +13 -2
- package/src/overlays/rewind-selector.ts +29 -12
- package/src/overlays/session-selector.ts +305 -39
- package/src/overlays/usage-dashboard.ts +5 -12
- package/src/prompt/composer.ts +213 -5
- package/src/prompt/custom-editor.ts +6 -1
- package/src/prompt/emoji-autocomplete.ts +0 -2
- package/src/prompt/interactive-context-helpers.ts +29 -4
- package/src/prompt/welcome.ts +31 -45
- package/src/render/extension-links.ts +214 -0
- package/src/render/hyperlink.ts +65 -9
- package/src/render/index.ts +1 -0
- package/src/setup/scenes/composer.ts +2 -0
- package/src/setup/scenes/glyph.ts +2 -0
- package/src/setup/scenes/model.ts +192 -29
- package/src/setup/scenes/outro.ts +30 -17
- package/src/setup/scenes/sign-in.ts +196 -400
- package/src/setup/scenes/splash.ts +25 -21
- package/src/setup/scenes/theme.ts +2 -0
- package/src/setup/scenes/types.ts +18 -1
- package/src/setup/startup-splash.ts +8 -7
- package/src/setup/wizard-overlay.ts +164 -33
- package/src/setup/wizard.ts +11 -2
- package/src/status-line/component.ts +4 -10
- package/src/status-line/context-usage.ts +72 -21
- package/src/status-line/host.ts +6 -2
- package/src/terminal.ts +24 -2
- package/src/theme/defaults.json +1 -0
- package/src/theme/loader.ts +1 -1
- package/src/theme/theme.ts +2 -75
- package/src/tools/bash-interactive.ts +6 -1
- package/src/tools/bash.ts +2 -0
- package/src/tools/default-renderer.ts +9 -6
- package/src/tools/eval.ts +14 -4
- package/src/tools/irc.ts +6 -0
- package/src/tools/job-widget.ts +13 -1
- package/src/tools/json-tree.ts +132 -42
- package/src/tools/lsp.ts +36 -15
- package/src/tools/mcp.ts +20 -12
- package/src/tools/native-view.ts +1 -1
- package/src/tools/renderer.ts +2 -0
- package/src/tools/streaming-output.ts +41 -0
- package/src/tools/task.ts +59 -17
- package/src/tools/url-scheme-host.ts +21 -0
- package/src/tools/yield.ts +18 -9
- package/src/tui.ts +43 -15
- package/THIRD-PARTY-NOTICES.txt +0 -22901
- package/dist/types/theme/defaults/index.d.ts +0 -10289
- package/src/theme/defaults/alabaster.json +0 -103
- package/src/theme/defaults/amethyst.json +0 -106
- package/src/theme/defaults/anthracite.json +0 -103
- package/src/theme/defaults/basalt.json +0 -101
- package/src/theme/defaults/birch.json +0 -105
- package/src/theme/defaults/dark-abyss.json +0 -101
- package/src/theme/defaults/dark-arctic.json +0 -114
- package/src/theme/defaults/dark-aurora.json +0 -105
- package/src/theme/defaults/dark-catppuccin.json +0 -117
- package/src/theme/defaults/dark-cavern.json +0 -101
- package/src/theme/defaults/dark-celestial.json +0 -116
- package/src/theme/defaults/dark-copper.json +0 -105
- package/src/theme/defaults/dark-cosmos.json +0 -100
- package/src/theme/defaults/dark-cyberpunk.json +0 -112
- package/src/theme/defaults/dark-dracula.json +0 -108
- package/src/theme/defaults/dark-eclipse.json +0 -101
- package/src/theme/defaults/dark-ember.json +0 -105
- package/src/theme/defaults/dark-equinox.json +0 -100
- package/src/theme/defaults/dark-forest.json +0 -106
- package/src/theme/defaults/dark-github.json +0 -115
- package/src/theme/defaults/dark-gruvbox.json +0 -122
- package/src/theme/defaults/dark-lavender.json +0 -105
- package/src/theme/defaults/dark-lunar.json +0 -99
- package/src/theme/defaults/dark-midnight.json +0 -105
- package/src/theme/defaults/dark-monochrome.json +0 -104
- package/src/theme/defaults/dark-monokai.json +0 -108
- package/src/theme/defaults/dark-nebula.json +0 -100
- package/src/theme/defaults/dark-neon-noir.json +0 -110
- package/src/theme/defaults/dark-nord.json +0 -108
- package/src/theme/defaults/dark-ocean.json +0 -111
- package/src/theme/defaults/dark-one.json +0 -110
- package/src/theme/defaults/dark-poimandres.json +0 -125
- package/src/theme/defaults/dark-rainforest.json +0 -101
- package/src/theme/defaults/dark-reef.json +0 -101
- package/src/theme/defaults/dark-retro.json +0 -102
- package/src/theme/defaults/dark-rose-pine.json +0 -106
- package/src/theme/defaults/dark-sakura.json +0 -105
- package/src/theme/defaults/dark-slate.json +0 -105
- package/src/theme/defaults/dark-solarized.json +0 -107
- package/src/theme/defaults/dark-solstice.json +0 -100
- package/src/theme/defaults/dark-starfall.json +0 -101
- package/src/theme/defaults/dark-sunset.json +0 -109
- package/src/theme/defaults/dark-swamp.json +0 -100
- package/src/theme/defaults/dark-synthwave.json +0 -113
- package/src/theme/defaults/dark-taiga.json +0 -101
- package/src/theme/defaults/dark-terminal.json +0 -105
- package/src/theme/defaults/dark-tokyo-night.json +0 -111
- package/src/theme/defaults/dark-tundra.json +0 -101
- package/src/theme/defaults/dark-twilight.json +0 -101
- package/src/theme/defaults/dark-volcanic.json +0 -101
- package/src/theme/defaults/graphite.json +0 -102
- package/src/theme/defaults/index.ts +0 -203
- package/src/theme/defaults/light-arctic.json +0 -117
- package/src/theme/defaults/light-aurora-day.json +0 -101
- package/src/theme/defaults/light-canyon.json +0 -101
- package/src/theme/defaults/light-catppuccin.json +0 -116
- package/src/theme/defaults/light-cirrus.json +0 -100
- package/src/theme/defaults/light-coral.json +0 -105
- package/src/theme/defaults/light-cyberpunk.json +0 -106
- package/src/theme/defaults/light-dawn.json +0 -100
- package/src/theme/defaults/light-dunes.json +0 -101
- package/src/theme/defaults/light-eucalyptus.json +0 -105
- package/src/theme/defaults/light-forest.json +0 -110
- package/src/theme/defaults/light-frost.json +0 -105
- package/src/theme/defaults/light-github.json +0 -125
- package/src/theme/defaults/light-glacier.json +0 -101
- package/src/theme/defaults/light-gruvbox.json +0 -118
- package/src/theme/defaults/light-haze.json +0 -100
- package/src/theme/defaults/light-honeycomb.json +0 -105
- package/src/theme/defaults/light-lagoon.json +0 -101
- package/src/theme/defaults/light-lavender.json +0 -105
- package/src/theme/defaults/light-meadow.json +0 -101
- package/src/theme/defaults/light-mint.json +0 -105
- package/src/theme/defaults/light-monochrome.json +0 -111
- package/src/theme/defaults/light-ocean.json +0 -109
- package/src/theme/defaults/light-one.json +0 -109
- package/src/theme/defaults/light-opal.json +0 -101
- package/src/theme/defaults/light-orchard.json +0 -101
- package/src/theme/defaults/light-paper.json +0 -105
- package/src/theme/defaults/light-poimandres.json +0 -125
- package/src/theme/defaults/light-prism.json +0 -100
- package/src/theme/defaults/light-retro.json +0 -108
- package/src/theme/defaults/light-sand.json +0 -105
- package/src/theme/defaults/light-savanna.json +0 -101
- package/src/theme/defaults/light-solarized.json +0 -112
- package/src/theme/defaults/light-soleil.json +0 -100
- package/src/theme/defaults/light-sunset.json +0 -109
- package/src/theme/defaults/light-synthwave.json +0 -108
- package/src/theme/defaults/light-tokyo-night.json +0 -121
- package/src/theme/defaults/light-wetland.json +0 -101
- package/src/theme/defaults/light-zenith.json +0 -99
- package/src/theme/defaults/limestone.json +0 -104
- package/src/theme/defaults/mahogany.json +0 -107
- package/src/theme/defaults/marble.json +0 -103
- package/src/theme/defaults/obsidian.json +0 -101
- package/src/theme/defaults/onyx.json +0 -101
- package/src/theme/defaults/pearl.json +0 -103
- package/src/theme/defaults/porcelain.json +0 -101
- package/src/theme/defaults/quartz.json +0 -106
- package/src/theme/defaults/sandstone.json +0 -105
- package/src/theme/defaults/titanium.json +0 -100
package/src/app-keybindings.ts
CHANGED
|
@@ -59,10 +59,13 @@ interface AppKeybindings {
|
|
|
59
59
|
"app.session.rename": true;
|
|
60
60
|
"app.session.delete": true;
|
|
61
61
|
"app.session.deleteNoninvasive": true;
|
|
62
|
+
"app.session.pin": true;
|
|
63
|
+
"app.session.unpin": true;
|
|
62
64
|
"app.tree.previousBranch": true;
|
|
63
65
|
"app.tree.nextBranch": true;
|
|
64
66
|
"app.plan.toggle": true;
|
|
65
67
|
"app.history.search": true;
|
|
68
|
+
"app.links.hint": true;
|
|
66
69
|
"app.stt.pushToTalk": true;
|
|
67
70
|
"app.stt.toggle": true;
|
|
68
71
|
"app.live.toggle": true;
|
|
@@ -239,6 +242,14 @@ export const KEYBINDINGS = {
|
|
|
239
242
|
defaultKeys: "ctrl+backspace",
|
|
240
243
|
description: "Delete session (non-invasive)",
|
|
241
244
|
},
|
|
245
|
+
"app.session.pin": {
|
|
246
|
+
defaultKeys: "alt+k",
|
|
247
|
+
description: "Pin the selected session in the resume picker",
|
|
248
|
+
},
|
|
249
|
+
"app.session.unpin": {
|
|
250
|
+
defaultKeys: "alt+shift+k",
|
|
251
|
+
description: "Unpin the selected session in the resume picker",
|
|
252
|
+
},
|
|
242
253
|
"app.tree.previousBranch": {
|
|
243
254
|
defaultKeys: [],
|
|
244
255
|
description: "Jump to the previous branch in /tree",
|
|
@@ -255,6 +266,10 @@ export const KEYBINDINGS = {
|
|
|
255
266
|
defaultKeys: "ctrl+r",
|
|
256
267
|
description: "Search history",
|
|
257
268
|
},
|
|
269
|
+
"app.links.hint": {
|
|
270
|
+
defaultKeys: "alt+o",
|
|
271
|
+
description: "Pick an extension link on screen by its hint tag",
|
|
272
|
+
},
|
|
258
273
|
"app.stt.pushToTalk": {
|
|
259
274
|
defaultKeys: "space",
|
|
260
275
|
description: "Hold to record speech-to-text",
|
|
@@ -11,6 +11,9 @@ export interface SessionPickerHost<T extends SessionSelectorEntry = SessionSelec
|
|
|
11
11
|
loadPinnedIds?(): Promise<ReadonlySet<string>>;
|
|
12
12
|
loadHistoryMatcher?(): SessionHistoryMatcher;
|
|
13
13
|
deleteSession?(session: T): Promise<boolean>;
|
|
14
|
+
/** Rename the session and resolve with the title as stored, or null when it did not happen. */
|
|
15
|
+
renameSession?(session: T, title: string): Promise<string | null>;
|
|
16
|
+
setSessionPinned?(session: T, pinned: boolean): Promise<void>;
|
|
14
17
|
loadAllSessions?(): Promise<T[]>;
|
|
15
18
|
}
|
|
16
19
|
|
|
@@ -21,6 +24,8 @@ export interface SessionPickerOptions<T extends SessionSelectorEntry = SessionSe
|
|
|
21
24
|
scopeLabel?: string | false;
|
|
22
25
|
showCwd?: boolean;
|
|
23
26
|
allowDelete?: boolean;
|
|
27
|
+
/** Rename the selected session; resolve with the title as stored, or null when it did not happen. */
|
|
28
|
+
onRename?: (session: T, title: string) => Promise<string | null>;
|
|
24
29
|
allowGlobalScope?: boolean;
|
|
25
30
|
historySearch?: boolean;
|
|
26
31
|
pinnedIds?: ReadonlySet<string>;
|
|
@@ -63,6 +68,8 @@ export async function selectSession<T extends SessionSelectorEntry>(
|
|
|
63
68
|
},
|
|
64
69
|
{
|
|
65
70
|
onDelete: options.allowDelete === false ? undefined : host.deleteSession,
|
|
71
|
+
onRename: options.onRename ?? host.renameSession,
|
|
72
|
+
onSetPinned: host.setSessionPinned,
|
|
66
73
|
historyMatcher,
|
|
67
74
|
loadAllSessions: options.allowGlobalScope === false ? undefined : host.loadAllSessions,
|
|
68
75
|
allSessions: options.allSessions,
|
package/src/charts/chart-plan.ts
CHANGED
|
@@ -33,7 +33,6 @@ import {
|
|
|
33
33
|
isNumber,
|
|
34
34
|
type NumberCell,
|
|
35
35
|
parseCell,
|
|
36
|
-
plainCell,
|
|
37
36
|
type Status,
|
|
38
37
|
type TableAnalysis,
|
|
39
38
|
type TableColumn,
|
|
@@ -536,20 +535,6 @@ const NEST_SEPARATORS = [" — ", " – ", ": ", " · ", ", ", " ", "/", "-", "_
|
|
|
536
535
|
/** Where a long row name's description starts (`Payroll (10 people…)`, `@watzon — #19, #53`). */
|
|
537
536
|
const DESCRIPTION = / [—–] | - |: | \(/;
|
|
538
537
|
|
|
539
|
-
/**
|
|
540
|
-
* Measures that improve upward, by header: pass and success rates, retrieval
|
|
541
|
-
* scores, throughput, speedups and savings.
|
|
542
|
-
*/
|
|
543
|
-
const HIGHER_BETTER =
|
|
544
|
-
/(?:^|[^\w])(?:pass(?:ed|es|ing)?|pass ?%|pass ?rate|success(?:es|ful)?|succeeded|accuracy|acc|recall|precision|prec|f1|em|exact[_ ]?match|scores?|wins?|win ?rate|hit ?rate|throughput|qps|rps|ops\/s|req\/s|tok\/s|tokens\/s|speedup|faster|improvement|savings?|saved|gain|coverage|quality|relevance|correct|solved|ok|renders|completion ?rate|uptime|performance|profits?|roi|pnl|growth|per \$|\/\$)(?![\w])/i;
|
|
545
|
-
/** Measures that improve downward, by header: cost, time, failures. */
|
|
546
|
-
const LOWER_BETTER =
|
|
547
|
-
/(?:^|[^\w])(?:cost|costs|price|bill|spend|usd|latency|time|duration|wall(?: ?clock)?|elapsed|errors?|err|fail(?:s|ed|ures?)?|timeouts?|miss(?:es)?|crash(?:es)?|regressions?|bugs?|warnings?|retries|turns|p\d{2}|blockers|violations|leaks?|stalls?|load|\$\/\w+)(?![\w])/i;
|
|
548
|
-
/** Resources a before/after change aims to shrink: lower is better only beside a baseline. */
|
|
549
|
-
const LOWER_IN_CHANGE =
|
|
550
|
-
/(?:^|[^\w])(?:tokens?|tok|rss|memory|mem|heap|allocs?|allocations?|clones|size|bytes|cpu|gc|input|output)(?![\w])/i;
|
|
551
|
-
/** Names of shares and counts that describe rather than score: no polarity whatever else they say. */
|
|
552
|
-
const NEUTRAL = /(?:^|[^\w])(?:share|shares|% of|portion|fraction|distribution|breakdown)(?![\w])/i;
|
|
553
538
|
/** Label headers naming rival options a table compares (models, configs, arms). */
|
|
554
539
|
const RIVALS =
|
|
555
540
|
/(?:^|[^\w])(?:models?|config(?:uration)?s?|arms?|variants?|approach(?:es)?|options?|plans?|polic(?:y|ies)|providers?|modes?|strateg(?:y|ies)|candidates?|settings?|shapes?|backends?|engines?|librar(?:y|ies)|implementations?|vendors?|tools?|methods?|algorithms?|setups?|runs?)(?![\w])/i;
|
|
@@ -606,13 +591,16 @@ export function planChart(table: TableAnalysis): ChartPlan | undefined {
|
|
|
606
591
|
|
|
607
592
|
/**
|
|
608
593
|
* Whether a model should pick `table`'s chart (`tui.autoGraph: smart`):
|
|
609
|
-
* multi-series tables,
|
|
610
|
-
* `guess` plots (`Thread | State`, `Finding | Current`),
|
|
611
|
-
* tells a
|
|
594
|
+
* multi-series tables, tables whose rows may be named by a number the
|
|
595
|
+
* `guess` plots (`Thread | State`, `Finding | Current`), and any other chart
|
|
596
|
+
* the guess would draw — only meaning tells a measure from page references,
|
|
597
|
+
* step numbers or figures quoted in prose, which the judge charts as none.
|
|
612
598
|
*/
|
|
613
599
|
export function shouldJudge(table: TableAnalysis, guess: ChartPlan | undefined): boolean {
|
|
614
600
|
if (!guess || table.rows.length < 3) return false;
|
|
615
|
-
|
|
601
|
+
if (table.measures.length >= 2 || leadingMeasures(table, guess).length > 0) return true;
|
|
602
|
+
const spec = buildChart(table, guess);
|
|
603
|
+
return spec !== undefined && worthCharting(spec);
|
|
616
604
|
}
|
|
617
605
|
|
|
618
606
|
/** Measure columns before `guess`'s text label column: numbers that may only name their rows. */
|
|
@@ -696,38 +684,33 @@ export function drawnNames(spec: ChartSpec): Set<string> {
|
|
|
696
684
|
}
|
|
697
685
|
|
|
698
686
|
/**
|
|
699
|
-
* The local guess at what `plan`'s rows and measures mean:
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
* `(current)` row names, and a status
|
|
687
|
+
* The local guess at what `plan`'s rows and measures mean: each measure's
|
|
688
|
+
* {@link Polarity} as far as its unit says ({@link dimPolarity}; what a name
|
|
689
|
+
* means — `Memory` as a capacity or a cost — is the judge's call), rivals from
|
|
690
|
+
* the label's header, focus from bolded or `(current)` row names, and a status
|
|
691
|
+
* column whose cells vary.
|
|
703
692
|
*/
|
|
704
693
|
export function guessMeaning(
|
|
705
694
|
table: TableAnalysis,
|
|
706
695
|
plan: ChartPlan,
|
|
707
696
|
): Pick<ChartPlan, "polarity" | "rowPolarity" | "rivals" | "focus" | "status"> {
|
|
708
697
|
const label = plan.label === undefined ? undefined : table.columns[plan.label];
|
|
709
|
-
// A comparison of two conditions reads
|
|
698
|
+
// A comparison of two conditions reads money and sizes the way a change does: as what it cuts.
|
|
710
699
|
const change =
|
|
711
700
|
plan.kind === "change" || plan.kind === "stat" || table.columns.some(column => BEFORE.test(column.header));
|
|
712
701
|
const polarity: Record<number, Polarity> = {};
|
|
713
702
|
for (const column of table.columns) {
|
|
714
|
-
if (column === label) continue;
|
|
715
703
|
// A signed delta's sign convention (saved vs. spent) is not in its unit.
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
(column.role === "measure" && !delta
|
|
720
|
-
? dimPolarity(column.dim, column.unit, change, column.scores)
|
|
721
|
-
: undefined);
|
|
704
|
+
if (column === label || column.role !== "measure" || DELTA.test(column.header) || column.signedShare >= 0.5)
|
|
705
|
+
continue;
|
|
706
|
+
const guessed = dimPolarity(column.dim, column.unit, change, column.scores);
|
|
722
707
|
if (guessed) polarity[column.index] = guessed;
|
|
723
708
|
}
|
|
724
709
|
const rowPolarity: Record<number, Polarity> = {};
|
|
725
710
|
if (label && rowsAreMetrics(table, plan))
|
|
726
711
|
for (const row of [...table.rows, ...table.totals]) {
|
|
727
712
|
const first = plan.series.map(index => table.columns[index]!.cells[row]).find(isNumber);
|
|
728
|
-
const guessed =
|
|
729
|
-
headerPolarity(label.cells[row]!.text, true) ??
|
|
730
|
-
(first ? dimPolarity(first.dim, first.unit, true, false) : undefined);
|
|
713
|
+
const guessed = first && dimPolarity(first.dim, first.unit, change, false);
|
|
731
714
|
if (guessed) rowPolarity[row] = guessed;
|
|
732
715
|
}
|
|
733
716
|
return {
|
|
@@ -745,23 +728,10 @@ export function rowsAreMetrics(table: TableAnalysis, plan: ChartPlan): boolean {
|
|
|
745
728
|
}
|
|
746
729
|
|
|
747
730
|
/**
|
|
748
|
-
* A measure's {@link Polarity} from its
|
|
749
|
-
* none); `change` admits resources a change aims to shrink.
|
|
750
|
-
*/
|
|
751
|
-
function headerPolarity(name: string, change: boolean): Polarity | undefined {
|
|
752
|
-
// `gtTagRelevance`: camelCase words read apart.
|
|
753
|
-
const plain = plainCell(name).text.replace(/([a-z])([A-Z])/g, "$1 $2");
|
|
754
|
-
if (NEUTRAL.test(plain)) return undefined;
|
|
755
|
-
if (HIGHER_BETTER.test(plain)) return "higher";
|
|
756
|
-
if (LOWER_BETTER.test(plain)) return "lower";
|
|
757
|
-
if (change && LOWER_IN_CHANGE.test(plain)) return "lower";
|
|
758
|
-
return undefined;
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
/**
|
|
762
|
-
* A measure's {@link Polarity} from its unit when its name gives none:
|
|
731
|
+
* A measure's {@link Polarity} from the unit its values are written in:
|
|
763
732
|
* scores out of a total and rates per second climb, durations shrink, and
|
|
764
|
-
* money and sizes shrink beside a baseline.
|
|
733
|
+
* money and sizes shrink beside a baseline (`change`). Elsewhere a size may
|
|
734
|
+
* be a capacity (`L2 cache`, `Memory`) and money a budget: no polarity.
|
|
765
735
|
*/
|
|
766
736
|
function dimPolarity(dim: Dimension | undefined, unit: string, change: boolean, scores: boolean): Polarity | undefined {
|
|
767
737
|
if (scores) return "higher";
|
|
@@ -2,6 +2,7 @@ import { type Component } from "../tui";
|
|
|
2
2
|
import { Disclosure } from "../components/disclosure";
|
|
3
3
|
import { visibleWidth } from "../utils";
|
|
4
4
|
import type { AdvisorMessageDetails, AdvisorNote, AdvisorSeverity } from "./messages";
|
|
5
|
+
import { chatTranscriptDisplayPreferences } from "./display-preferences";
|
|
5
6
|
import { formatBadge, replaceTabs, type ToolUIColor, wrapTextWithAnsi } from "../render/render-utils";
|
|
6
7
|
import { Ellipsis, truncateToWidth } from "../render";
|
|
7
8
|
import type { Theme } from "../theme";
|
|
@@ -274,6 +275,7 @@ export function createAdvisorMessageCard(
|
|
|
274
275
|
body: () => new AdvisorNotes(notes, 0, uiTheme),
|
|
275
276
|
expanded: getExpanded(),
|
|
276
277
|
paddingX: 1,
|
|
278
|
+
maxCollapsedRows: chatTranscriptDisplayPreferences.advisorCollapsedLines || undefined,
|
|
277
279
|
});
|
|
278
280
|
// The tool-output toggle owns expansion state; synchronize the controlled
|
|
279
281
|
// disclosure from the callback on every render.
|
|
@@ -5,6 +5,7 @@ import { Image, type ImageBudget } from "../components/image";
|
|
|
5
5
|
import { ImageProtocol, TERMINAL } from "../terminal-capabilities";
|
|
6
6
|
import {
|
|
7
7
|
type DefaultTextStyle,
|
|
8
|
+
endsInsideOpenFence,
|
|
8
9
|
holdPartialTableRow,
|
|
9
10
|
Markdown,
|
|
10
11
|
type MarkdownTheme,
|
|
@@ -14,10 +15,10 @@ import {
|
|
|
14
15
|
} from "../components/markdown";
|
|
15
16
|
import { Spacer } from "../components/spacer";
|
|
16
17
|
import { Text } from "../components/text";
|
|
17
|
-
import { formatDuration, formatNumber } from "@oh-my-pi/pi-utils";
|
|
18
|
+
import { formatDuration, formatNumber, logger } from "@oh-my-pi/pi-utils";
|
|
18
19
|
import chalk from "@oh-my-pi/pi-utils/chalk";
|
|
19
20
|
import { LRUCache } from "@oh-my-pi/pi-utils/lru";
|
|
20
|
-
import type { AssistantThinkingRenderer } from "./extension-types";
|
|
21
|
+
import type { AssistantTextRenderer, AssistantTextRendering, AssistantThinkingRenderer } from "./extension-types";
|
|
21
22
|
import { ensureThemeSync, getMarkdownTheme, getMarkdownThemeWithLinkTargets, getThemeEpoch, theme } from "../theme";
|
|
22
23
|
import { card, col, elapsed, node, span, text } from "../native/describe";
|
|
23
24
|
import { hasTranscriptActions, runTranscriptAction } from "./transcript-actions";
|
|
@@ -51,6 +52,8 @@ import { chartFence, hasChartTable, lookupTableChart, splitTableCharts } from ".
|
|
|
51
52
|
*/
|
|
52
53
|
const MAX_TRANSCRIPT_ERROR_ROWS = 8;
|
|
53
54
|
const EMPTY_STABLE_RENDER: readonly string[] = [];
|
|
55
|
+
/** Text renderers that threw once; each failure is logged once per renderer, not per frame. */
|
|
56
|
+
const failedTextRenderers = new WeakSet<AssistantTextRenderer>();
|
|
54
57
|
|
|
55
58
|
/** The native head of a finished thinking block: "Thought for 12s", or "Thought" when it was never seen streaming. */
|
|
56
59
|
function thoughtLabel(clock: { start: number; end?: number } | undefined): string {
|
|
@@ -245,6 +248,19 @@ function lerpHex(from: string, to: string, t: number): string {
|
|
|
245
248
|
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
|
246
249
|
}
|
|
247
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Drop the closing border the renderer draws for a source that ends inside an
|
|
253
|
+
* open fence. That row is not part of the message yet: the live block reaches
|
|
254
|
+
* it only when the closer arrives, so publishing it would make the settled
|
|
255
|
+
* prefix disagree with the block render and freeze mid-stream retirement.
|
|
256
|
+
*/
|
|
257
|
+
function trimFabricatedCloser(rows: readonly string[], source: string): readonly string[] {
|
|
258
|
+
if (!endsInsideOpenFence(source)) return rows;
|
|
259
|
+
let last = rows.length;
|
|
260
|
+
while (last > 0 && rows[last - 1]!.trim() === "") last--;
|
|
261
|
+
return last > 0 ? rows.slice(0, last - 1) : rows;
|
|
262
|
+
}
|
|
263
|
+
|
|
248
264
|
/**
|
|
249
265
|
* Renders an assistant message; streaming content remains mutable until the
|
|
250
266
|
* provider finalizes it because later deltas can revise earlier Markdown.
|
|
@@ -390,8 +406,10 @@ export class AssistantMessageComponent extends Container {
|
|
|
390
406
|
#reactionTarget: ReactionTarget | undefined;
|
|
391
407
|
/** Reaction lifted from the reply's opening emoji, once resolved. */
|
|
392
408
|
#reaction: string | undefined;
|
|
393
|
-
/** Display form of {@link #lastMessage} (reaction handled) the native description is built from. */
|
|
409
|
+
/** Display form of {@link #lastMessage} (reaction handled, extension text renderers applied) the native description is built from. */
|
|
394
410
|
#displayedMessage: AssistantMessage | undefined;
|
|
411
|
+
/** {@link #displayedMessage} before extension text renderers: the reply as written, which thinking state and the native Copy follow. */
|
|
412
|
+
#answerMessage: AssistantMessage | undefined;
|
|
395
413
|
/** Thinking-extension components per content index, recorded when the slow path mounts them. */
|
|
396
414
|
#thinkingExtensions = new Map<number, Component[]>();
|
|
397
415
|
/** Collapse state of thinking sections toggled in the terminal, by content index; cleared by {@link setHideThinkingBlock}. */
|
|
@@ -523,9 +541,65 @@ export class AssistantMessageComponent extends Container {
|
|
|
523
541
|
content[index] = { ...block, text };
|
|
524
542
|
return { ...message, content };
|
|
525
543
|
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Display form with the extension text renderers applied, in registration order: each gets the
|
|
547
|
+
* previous one's output. A renderer that throws or returns a non-string leaves its input drawn;
|
|
548
|
+
* a throw is logged once per renderer. A block whose text and `streaming` flag are unchanged
|
|
549
|
+
* since its last render reuses that output: a block sealed at a tool call keeps receiving
|
|
550
|
+
* updates while the call's arguments stream. {@link rerenderMessage} and
|
|
551
|
+
* {@link setTextRenderers} drop the reused output.
|
|
552
|
+
*/
|
|
553
|
+
#renderText(message: AssistantMessage, streaming: boolean): AssistantMessage {
|
|
554
|
+
const rendering = this.#textRendering;
|
|
555
|
+
if (!rendering) return message;
|
|
556
|
+
// Renderers are display-only: hand them a deep copy so no edit, however nested, reaches the stored message.
|
|
557
|
+
let view: AssistantMessage | undefined;
|
|
558
|
+
let content: AssistantMessage["content"] | undefined;
|
|
559
|
+
// Blocks stream in order: only the last one can still grow.
|
|
560
|
+
const tail = message.content.length - 1;
|
|
561
|
+
for (let index = 0; index < message.content.length; index++) {
|
|
562
|
+
const block = message.content[index]!;
|
|
563
|
+
if (block.type !== "text" || block.text.length === 0) continue;
|
|
564
|
+
const blockStreaming = streaming && index === tail;
|
|
565
|
+
const cached = this.#renderedText.get(index);
|
|
566
|
+
let text: string;
|
|
567
|
+
if (cached?.input === block.text && cached.streaming === blockStreaming) {
|
|
568
|
+
text = cached.output;
|
|
569
|
+
} else {
|
|
570
|
+
view ??= structuredClone(message);
|
|
571
|
+
text = block.text;
|
|
572
|
+
for (const renderer of rendering.renderers) {
|
|
573
|
+
try {
|
|
574
|
+
const rendered = renderer({
|
|
575
|
+
message: view,
|
|
576
|
+
contentIndex: rendering.contentOffset + index,
|
|
577
|
+
text,
|
|
578
|
+
streaming: blockStreaming,
|
|
579
|
+
});
|
|
580
|
+
if (typeof rendered === "string") text = rendered;
|
|
581
|
+
} catch (error) {
|
|
582
|
+
if (failedTextRenderers.has(renderer)) continue;
|
|
583
|
+
failedTextRenderers.add(renderer);
|
|
584
|
+
logger.warn("Assistant text renderer failed; drawing its input", { error: String(error) });
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
this.#renderedText.set(index, { input: block.text, streaming: blockStreaming, output: text });
|
|
588
|
+
}
|
|
589
|
+
if (text === block.text) continue;
|
|
590
|
+
content ??= message.content.slice();
|
|
591
|
+
content[index] = { ...block, text };
|
|
592
|
+
}
|
|
593
|
+
return content ? { ...message, content } : message;
|
|
594
|
+
}
|
|
526
595
|
#hideThinkingBlock: boolean;
|
|
527
596
|
readonly #onImageUpdate?: () => void;
|
|
528
597
|
readonly #thinkingRenderers: readonly AssistantThinkingRenderer[];
|
|
598
|
+
#textRendering: AssistantTextRendering | undefined;
|
|
599
|
+
/** Index in the full reply's `content` of this component's first block, kept across renderer swaps. */
|
|
600
|
+
readonly #textContentOffset: number;
|
|
601
|
+
/** Last renderer output per content index, reused while the block's input and `streaming` flag hold; see {@link #renderText}. */
|
|
602
|
+
readonly #renderedText = new Map<number, { input: string; streaming: boolean; output: string }>();
|
|
529
603
|
readonly #imageBudget?: ImageBudget;
|
|
530
604
|
#proseOnlyThinking: boolean;
|
|
531
605
|
#expandThinkingBlocks: boolean;
|
|
@@ -539,11 +613,14 @@ export class AssistantMessageComponent extends Container {
|
|
|
539
613
|
proseOnlyThinking = true,
|
|
540
614
|
linkTargets?: ReadonlyMap<string, string>,
|
|
541
615
|
expandThinkingBlocks = false,
|
|
616
|
+
textRendering?: AssistantTextRendering,
|
|
542
617
|
) {
|
|
543
618
|
super();
|
|
544
619
|
this.#hideThinkingBlock = hideThinkingBlock;
|
|
545
620
|
this.#onImageUpdate = onImageUpdate;
|
|
546
621
|
this.#thinkingRenderers = thinkingRenderers;
|
|
622
|
+
this.#textRendering = textRendering?.renderers.length ? textRendering : undefined;
|
|
623
|
+
this.#textContentOffset = textRendering?.contentOffset ?? 0;
|
|
547
624
|
this.#imageBudget = imageBudget;
|
|
548
625
|
this.#proseOnlyThinking = proseOnlyThinking;
|
|
549
626
|
this.#expandThinkingBlocks = expandThinkingBlocks;
|
|
@@ -792,8 +869,9 @@ export class AssistantMessageComponent extends Container {
|
|
|
792
869
|
return this.#transcriptBlockFinalized;
|
|
793
870
|
}
|
|
794
871
|
|
|
795
|
-
/**
|
|
872
|
+
/** Hold image and figure rendering until the completed block can enter immutable scrollback. */
|
|
796
873
|
isTranscriptBlockPending(): boolean {
|
|
874
|
+
if (this.#showImages && this.#kittyConversionsAwaited.size > 0) return true;
|
|
797
875
|
for (const block of this.#figureBlocks.values()) if (block.pending) return true;
|
|
798
876
|
return false;
|
|
799
877
|
}
|
|
@@ -815,7 +893,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
815
893
|
*/
|
|
816
894
|
override describe(cx?: DescribeContext): NativeNode {
|
|
817
895
|
const media = cx?.supports("media") === true;
|
|
818
|
-
const tail = this.#
|
|
896
|
+
const tail = this.#answerMessage ? this.#thinkingTailIndex(this.#answerMessage) : undefined;
|
|
819
897
|
const rate =
|
|
820
898
|
tail !== undefined && this.#lastUpdateTransient && this.#thinkingRateLive
|
|
821
899
|
? Math.round(Math.min(SPEED_MAX, sharedSpeedTracker.getSpeed()) * 10) / 10
|
|
@@ -886,13 +964,15 @@ export class AssistantMessageComponent extends Container {
|
|
|
886
964
|
let answered = false;
|
|
887
965
|
if (message) {
|
|
888
966
|
const live = !this.#transcriptBlockFinalized && this.#lastUpdateTransient;
|
|
967
|
+
// Which block streams, and whether there is an answer to copy, follow the reply as written:
|
|
968
|
+
// text renderers change only how it is drawn (one may hide all of it).
|
|
969
|
+
const answer = this.#answerMessage ?? message;
|
|
889
970
|
let tailIndex = -1;
|
|
890
|
-
for (let index = 0; index <
|
|
891
|
-
const content =
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
) {
|
|
971
|
+
for (let index = 0; index < answer.content.length; index++) {
|
|
972
|
+
const content = answer.content[index]!;
|
|
973
|
+
const answerText = content.type === "text" && canonicalizeMessage(content.text) !== "";
|
|
974
|
+
answered ||= answerText;
|
|
975
|
+
if (answerText || (content.type === "thinking" && canonicalizeMessage(content.thinking))) {
|
|
896
976
|
tailIndex = index;
|
|
897
977
|
}
|
|
898
978
|
}
|
|
@@ -900,7 +980,6 @@ export class AssistantMessageComponent extends Container {
|
|
|
900
980
|
const content = message.content[index]!;
|
|
901
981
|
const streaming = live && index === tailIndex;
|
|
902
982
|
if (content.type === "text" && canonicalizeMessage(content.text)) {
|
|
903
|
-
answered = true;
|
|
904
983
|
// A native terminal sizes table columns by every row it gets: send a growing row once
|
|
905
984
|
// its line ends. Hold before trimming, which would drop the newline ending a finished row.
|
|
906
985
|
const source = (streaming ? holdPartialTableRow(content.text) : content.text).trim();
|
|
@@ -1152,7 +1231,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
1152
1231
|
/** The answer as the transcript shows it: its text blocks' markdown, a blank line apart, without thinking or tool calls. */
|
|
1153
1232
|
#answerText(): string {
|
|
1154
1233
|
const blocks: string[] = [];
|
|
1155
|
-
for (const content of this.#
|
|
1234
|
+
for (const content of this.#answerMessage?.content ?? []) {
|
|
1156
1235
|
if (content.type === "text" && canonicalizeMessage(content.text)) blocks.push(content.text.trim());
|
|
1157
1236
|
}
|
|
1158
1237
|
return blocks.join("\n\n");
|
|
@@ -1214,7 +1293,8 @@ export class AssistantMessageComponent extends Container {
|
|
|
1214
1293
|
* every guard skips publication and nothing ever retracts it.
|
|
1215
1294
|
*/
|
|
1216
1295
|
#publishStableSnapshot(rendered: readonly string[], width: number): void {
|
|
1217
|
-
|
|
1296
|
+
// Extension text renderers may rewrite text they already drew, and published rows never retract.
|
|
1297
|
+
if (!this.#midStreamPublication || this.#textRendering) return;
|
|
1218
1298
|
const parts = this.#currentStableSnapshot();
|
|
1219
1299
|
if (!parts) return;
|
|
1220
1300
|
const last = parts.at(-1);
|
|
@@ -1359,12 +1439,12 @@ export class AssistantMessageComponent extends Container {
|
|
|
1359
1439
|
const renderer = role === "head" ? head : role === "replay" ? replay : head?.index === index ? head : replay;
|
|
1360
1440
|
if (renderer?.index === index && renderer.kind === kind) {
|
|
1361
1441
|
renderer.md.setText(trimmed);
|
|
1362
|
-
return renderer.md.render(width);
|
|
1442
|
+
return trimFabricatedCloser(renderer.md.render(width), trimmed);
|
|
1363
1443
|
}
|
|
1364
1444
|
const md = this.#createMarkdown(kind, trimmed);
|
|
1365
1445
|
if (role === "head") this.#stableHeadRenderer = { index, kind, md };
|
|
1366
1446
|
else if (role === "replay") this.#stableReplayRenderer = { index, kind, md };
|
|
1367
|
-
return md.render(width);
|
|
1447
|
+
return trimFabricatedCloser(md.render(width), trimmed);
|
|
1368
1448
|
}
|
|
1369
1449
|
|
|
1370
1450
|
/** Markdown for a text or thinking block; live children and stable-row renders share it so stable rows prefix the block render. */
|
|
@@ -1491,11 +1571,30 @@ export class AssistantMessageComponent extends Container {
|
|
|
1491
1571
|
this.#stopThinkingAnimation();
|
|
1492
1572
|
// If the live pulse was on screen when the block sealed, drop the fast path
|
|
1493
1573
|
// and rebuild so the placeholder is removed — finalized blocks never animate.
|
|
1574
|
+
// A sealed block's text is also complete and can retire to scrollback before the
|
|
1575
|
+
// reply's final update reaches it: give the text renderers their final call now
|
|
1576
|
+
// (sealed renders pass `streaming: false`).
|
|
1577
|
+
const sealRenderers = this.#textRendering !== undefined && this.#lastUpdateTransient;
|
|
1494
1578
|
if (this.#thinkingDots) {
|
|
1495
1579
|
this.#fastPathKey = undefined;
|
|
1496
1580
|
this.#fastPathItems = undefined;
|
|
1497
|
-
if (this.#lastMessage) this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
|
|
1498
1581
|
}
|
|
1582
|
+
if ((this.#thinkingDots || sealRenderers) && this.#lastMessage) {
|
|
1583
|
+
this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
/**
|
|
1588
|
+
* Swap the extension text renderers (the setting enabling them changed) and redraw with them.
|
|
1589
|
+
* Enabling them also drops rows already published as stable, since a renderer may rewrite them;
|
|
1590
|
+
* callers MUST pair this with a scrollback-clearing display reset, as for
|
|
1591
|
+
* {@link resetTranscriptStableRows}.
|
|
1592
|
+
*/
|
|
1593
|
+
setTextRenderers(renderers: readonly AssistantTextRenderer[]): void {
|
|
1594
|
+
this.#textRendering = renderers.length ? { renderers, contentOffset: this.#textContentOffset } : undefined;
|
|
1595
|
+
this.#renderedText.clear();
|
|
1596
|
+
if (this.#textRendering) this.resetTranscriptStableRows();
|
|
1597
|
+
if (this.#lastMessage) this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
|
|
1499
1598
|
}
|
|
1500
1599
|
|
|
1501
1600
|
applyRetryRecovery(retryRecovery: AssistantMessage["retryRecovery"]): void {
|
|
@@ -1504,6 +1603,18 @@ export class AssistantMessageComponent extends Container {
|
|
|
1504
1603
|
this.updateContent({ ...this.#lastMessage, retryRecovery });
|
|
1505
1604
|
}
|
|
1506
1605
|
|
|
1606
|
+
/**
|
|
1607
|
+
* Draw the current message again when it is the reply sent at `timestamp`, re-running the
|
|
1608
|
+
* extension text renderers; returns whether it was. The caller schedules the frame.
|
|
1609
|
+
*/
|
|
1610
|
+
rerenderMessage(timestamp: number): boolean {
|
|
1611
|
+
if (this.#lastMessage?.timestamp !== timestamp) return false;
|
|
1612
|
+
// The renderers' state may have changed: run them again rather than reuse their last output.
|
|
1613
|
+
this.#renderedText.clear();
|
|
1614
|
+
this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
|
|
1615
|
+
return true;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1507
1618
|
messagePersistenceKey(): string | undefined {
|
|
1508
1619
|
if (!this.#lastMessage) return undefined;
|
|
1509
1620
|
return [
|
|
@@ -1615,6 +1726,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
1615
1726
|
})
|
|
1616
1727
|
.catch(() => {
|
|
1617
1728
|
this.#kittyConversionsAwaited.delete(key);
|
|
1729
|
+
this.#onImageUpdate?.();
|
|
1618
1730
|
});
|
|
1619
1731
|
}
|
|
1620
1732
|
}
|
|
@@ -1806,6 +1918,12 @@ export class AssistantMessageComponent extends Container {
|
|
|
1806
1918
|
// verbatim message so re-renders re-derive the reaction deterministically.
|
|
1807
1919
|
// Narration the provider returned as thinking renders as answer text.
|
|
1808
1920
|
message = this.#displayMessage(presentNarrationAsAnswer(message), this.#lastUpdateTransient);
|
|
1921
|
+
this.#answerMessage = message;
|
|
1922
|
+
// A sealed block (closed at a tool call) keeps receiving transient updates; its text is final.
|
|
1923
|
+
// Thinking state below reads `answer`: renderers transform display only, so one that hides the
|
|
1924
|
+
// streaming text must not make the closed thought before it look live again.
|
|
1925
|
+
const answer = message;
|
|
1926
|
+
message = this.#renderText(message, this.#lastUpdateTransient && !this.#transcriptBlockFinalized);
|
|
1809
1927
|
this.#displayedMessage = message;
|
|
1810
1928
|
|
|
1811
1929
|
// Streaming-speed gauge: only a live, in-flight render of the single
|
|
@@ -1823,7 +1941,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
1823
1941
|
// Native terminals show the live rate on visible thinking too, not only the hidden pulse.
|
|
1824
1942
|
const isThinkingNow =
|
|
1825
1943
|
this.#lastUpdateTransient &&
|
|
1826
|
-
(isNativeRendering() ? this.#thinkingTailIndex(
|
|
1944
|
+
(isNativeRendering() ? this.#thinkingTailIndex(answer) !== undefined : this.#shouldAnimateThinking(answer));
|
|
1827
1945
|
if (isThinkingNow) {
|
|
1828
1946
|
const currentTokens = message.usage.reasoningTokens ?? message.usage.output;
|
|
1829
1947
|
this.#thinkingTokens = currentTokens;
|
|
@@ -1926,7 +2044,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
1926
2044
|
if (this.#figureBlocks.get(index) !== block) block.dispose();
|
|
1927
2045
|
}
|
|
1928
2046
|
|
|
1929
|
-
if (this.#shouldAnimateThinking(
|
|
2047
|
+
if (this.#shouldAnimateThinking(answer)) {
|
|
1930
2048
|
if (hasVisibleContent) this.#contentContainer.addChild(new Spacer(1));
|
|
1931
2049
|
this.#thinkingDots = new Text(this.#thinkingDotsLabel(), 1, 0);
|
|
1932
2050
|
this.#contentContainer.addChild(this.#thinkingDots);
|
|
@@ -3,6 +3,7 @@ import { replaceTabs } from "../render/render-utils";
|
|
|
3
3
|
import { theme } from "../theme";
|
|
4
4
|
import type { TranscriptBlock } from "../chrome/transcript-container";
|
|
5
5
|
import { TranscriptStatusBlock } from "../chrome/transcript-status";
|
|
6
|
+
import { span } from "../native/describe";
|
|
6
7
|
|
|
7
8
|
const TAN_WORK_PREVIEW_LENGTH = 56;
|
|
8
9
|
|
|
@@ -22,6 +23,8 @@ export function createBackgroundTanDispatchBlock(message: CustomMessage<unknown>
|
|
|
22
23
|
const details = (message as CustomMessage<Partial<BackgroundTanDispatchDetails>>).details;
|
|
23
24
|
const jobId = details?.jobId ?? "unknown";
|
|
24
25
|
const work = details?.work ? previewWork(details.work) : undefined;
|
|
26
|
+
const spans = [span("Tangent dispatched"), span(" · ", "dim"), span(`task ${jobId}`, "dim")];
|
|
27
|
+
if (work) spans.push(span(" · ", "dim"), span(work, "dim"));
|
|
25
28
|
return new TranscriptStatusBlock([
|
|
26
29
|
{
|
|
27
30
|
parts: [
|
|
@@ -30,6 +33,7 @@ export function createBackgroundTanDispatchBlock(message: CustomMessage<unknown>
|
|
|
30
33
|
{ text: jobId, color: "accent" },
|
|
31
34
|
work ? { text: `${theme.format.dash} ${work}`, color: "dim" } : undefined,
|
|
32
35
|
],
|
|
36
|
+
native: { tone: "info", icon: "output", message: spans },
|
|
33
37
|
},
|
|
34
38
|
]);
|
|
35
39
|
}
|
|
@@ -16,6 +16,7 @@ import type { TruncationMeta } from "../tools/output-meta";
|
|
|
16
16
|
import { resolveImageOptions } from "../render/render-utils";
|
|
17
17
|
import { OutputPane } from "../render/output-pane";
|
|
18
18
|
import { loadXtermTerminal, readTerminalRows, styleTerminalRow } from "../tools/terminal-output";
|
|
19
|
+
import { normalizeInputForPty } from "../tools/bash-interactive";
|
|
19
20
|
import { getSixelLineMask, isSixelPassthroughEnabled, sanitizeWithOptionalSixelPassthrough } from "../render/sixel";
|
|
20
21
|
import {
|
|
21
22
|
buildExecutionFrame,
|
|
@@ -286,6 +287,11 @@ export class BashExecutionComponent extends Container {
|
|
|
286
287
|
this.#drainPtyQueue();
|
|
287
288
|
}
|
|
288
289
|
|
|
290
|
+
/** Bytes a terminal would send for a key event, honoring the command's cursor-key mode; "" for key releases. */
|
|
291
|
+
encodePtyInput(data: string): string {
|
|
292
|
+
return normalizeInputForPty(data, this.#ptyTerminal?.modes.applicationCursorKeysMode ?? false);
|
|
293
|
+
}
|
|
294
|
+
|
|
289
295
|
#drainPtyQueue(): void {
|
|
290
296
|
const terminal = this.#ptyTerminal;
|
|
291
297
|
if (!terminal || this.#ptyWriting) return;
|