@oh-my-pi/pi-tui 18.4.9 → 18.4.11

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/types/chat/assistant-message.d.ts +4 -3
  3. package/dist/types/chat/messages.d.ts +4 -0
  4. package/dist/types/chrome/transcript-container.d.ts +7 -0
  5. package/dist/types/components/markdown.d.ts +0 -4
  6. package/dist/types/overlays/advisor-config.d.ts +8 -0
  7. package/dist/types/overlays/ask-dialog.d.ts +1 -0
  8. package/dist/types/prompt/composer.d.ts +3 -1
  9. package/dist/types/status-line/context-usage.d.ts +2 -0
  10. package/dist/types/terminal-capabilities.d.ts +5 -0
  11. package/dist/types/tools/agent-tree.d.ts +2 -0
  12. package/dist/types/tools/task.d.ts +2 -0
  13. package/dist/types/tools/wait.d.ts +2 -0
  14. package/dist/types/tools/write.d.ts +11 -16
  15. package/dist/types/tui.d.ts +3 -0
  16. package/dist/types/windows-input-mode.d.ts +13 -2
  17. package/package.json +9 -9
  18. package/src/chat/advisor-message.ts +4 -2
  19. package/src/chat/assistant-message.ts +7 -9
  20. package/src/chat/chat-transcript-builder.ts +1 -1
  21. package/src/chat/messages.ts +4 -0
  22. package/src/chrome/transcript-container.ts +13 -0
  23. package/src/components/markdown.ts +90 -130
  24. package/src/keys.ts +2 -4
  25. package/src/native/encode.ts +2 -0
  26. package/src/overlays/advisor-config.ts +228 -2
  27. package/src/overlays/ask-dialog.ts +1 -0
  28. package/src/overlays/model-hub.ts +25 -4
  29. package/src/overlays/session-info-overlay.ts +55 -11
  30. package/src/prompt/composer.ts +32 -7
  31. package/src/status-line/context-usage.ts +18 -2
  32. package/src/terminal-capabilities.ts +8 -0
  33. package/src/terminal.ts +12 -10
  34. package/src/tools/agent-tree.ts +5 -0
  35. package/src/tools/eval.ts +14 -5
  36. package/src/tools/task.ts +4 -0
  37. package/src/tools/wait.ts +24 -22
  38. package/src/tools/write.ts +39 -4
  39. package/src/tui.ts +4 -0
  40. package/src/windows-input-mode.ts +26 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,43 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.4.11] - 2026-10-02
6
+
7
+ ### Added
8
+
9
+ - Added subagent completion percentages to the agent tree, task, and wait views.
10
+
11
+ ### Changed
12
+
13
+ - Updated notebook evaluation cells in native hosts to use directional gutter indicators for inputs and outputs, with a progress indicator while a cell is running.
14
+
15
+ ### Fixed
16
+
17
+ - Fixed multiline paste on Windows so pasted line breaks are inserted correctly instead of terminal escape sequences.
18
+
19
+ ## [18.4.10] - 2026-10-02
20
+
21
+ ### Added
22
+
23
+ - Added ContextUsageView.setBreakdown to refresh usage card without recreating it
24
+ - Added support for change events with flexible values in native TUI
25
+
26
+ ### Fixed
27
+
28
+ - Keep settled responses reachable in scrollback while an ask panel is open, and keep the editor at the bottom after answering ([#12398](https://github.com/can1357/oh-my-pi/issues/12398), [#13993](https://github.com/can1357/oh-my-pi/pull/13993) by [@Dante-dan](https://github.com/Dante-dan)).
29
+ - Fixed a finished `wait` whose jobs were all still running going blank, and the next `wait` removing it while its turn's usage row stayed; the card now keeps its job snapshot ([#12248](https://github.com/can1357/oh-my-pi/issues/12248), [#13978](https://github.com/can1357/oh-my-pi/pull/13978) by [@Dante-dan](https://github.com/Dante-dan)).
30
+ - Hidden thinking blocks no longer leave a faint "Thought for Ns" row in Tern's native transcript; only the live "Thinking…" indicator shows while the model reasons.
31
+ - Fixed long Markdown paragraphs, such as a read preview of a file with no blank line, stalling rendering: a 44 KB paragraph now renders in about 8 ms instead of 95 ms ([#13961](https://github.com/can1357/oh-my-pi/pull/13961) by [@sjawhar](https://github.com/sjawhar)).
32
+ - Fixed Markdown paragraphs with many unclosed `[`, `*` or `_`, or with a long address-like word, stalling rendering for seconds ([#13961](https://github.com/can1357/oh-my-pi/pull/13961) by [@sjawhar](https://github.com/sjawhar)).
33
+ - Fixed Markdown paragraphs with many unclosed `~~`, `$`, `\(` or `\[`, nested brackets or emphasis, or unclosed HTML, stalling rendering for seconds: 40 KB of unclosed `~~` took 85 s. Emphasis or links nested a thousand levels deep still render slowly, for seconds per few KB: each level restyles the text inside it ([#13961](https://github.com/can1357/oh-my-pi/pull/13961) by [@sjawhar](https://github.com/sjawhar)).
34
+ - Fixed arrow keys acting as Escape and terminal query replies appearing as typed text when running omp on Windows over SSH ([#14034](https://github.com/can1357/oh-my-pi/issues/14034)).
35
+ - Fixed `/model` under an `enabledModels`/`--models` scope hiding every judge, search, image, and speech model and dropping their configured role assignments (JUDGE, WEB, IMAGE, …) ([#14016](https://github.com/can1357/oh-my-pi/issues/14016))
36
+
37
+ ### Removed
38
+
39
+ - Removed the internal `urlTokenPossible` export ([#13961](https://github.com/can1357/oh-my-pi/pull/13961) by [@sjawhar](https://github.com/sjawhar)).
40
+ - Removed the internal `autolinkSchemeScanIndex` export ([#13961](https://github.com/can1357/oh-my-pi/pull/13961) by [@sjawhar](https://github.com/sjawhar)).
41
+
5
42
  ## [18.4.9] - 2026-10-01
6
43
 
7
44
  ### Added
@@ -86,9 +86,10 @@ export declare class AssistantMessageComponent extends Container {
86
86
  * A `col` (role `omp.assistant`) of `md` nodes keyed by content index, so
87
87
  * streamed deltas reach the terminal as `text append` on the same node;
88
88
  * the tail block carries `stream: true` until the message finalizes.
89
- * Thinking blocks are quiet collapsible `section`s (collapsed per
90
- * `hideThinkingBlock`): a muted "Thinking…" shimmer while live, then
91
- * "Thought for 12s"; tokens and rate ride in the head's `title`.
89
+ * Thinking blocks are quiet collapsible `section`s: a muted "Thinking…"
90
+ * shimmer while live, then "Thought for 12s"; tokens and rate ride in the
91
+ * head's `title`. With `hideThinkingBlock` only the live head shows (collapsed)
92
+ * and a finished thought emits nothing.
92
93
  */
93
94
  describe(): NativeNode;
94
95
  /**
@@ -180,6 +180,10 @@ export interface AdvisorNote {
180
180
  severity?: AdvisorSeverity;
181
181
  /** Which configured advisor produced this note (omitted for the default advisor). */
182
182
  advisor?: string;
183
+ /** Primary turns completed when note was emitted. */
184
+ turn?: number;
185
+ /** Primary turns elapsed before merged-batch delivery. */
186
+ turnsAgo?: number;
183
187
  }
184
188
  /** Details payload on the batched `advisor` custom message rendered in the transcript. */
185
189
  export interface AdvisorMessageDetails {
@@ -81,6 +81,13 @@ export declare class TranscriptContainer extends Container {
81
81
  resetStableEmission(): void;
82
82
  /** Whether a transient block may be discarded without leaving tape history. */
83
83
  canRemoveBlock(component: Component): boolean;
84
+ /**
85
+ * Whether a superseded snapshot block (a repeated `wait` poll) may be
86
+ * replaced by its successor. Beyond {@link canRemoveBlock}, a block directly
87
+ * above a usage row stays: that row describes the turn the block belongs to,
88
+ * and removing the block would leave a usage-only turn (#12248).
89
+ */
90
+ canDisplaceBlock(component: Component): boolean;
84
91
  /** Lifecycle state per block in transcript order (diagnostics and tests). */
85
92
  blockStates(): readonly BlockState[];
86
93
  /** Permanently captured presentation mode per block (diagnostics and tests). */
@@ -1,10 +1,6 @@
1
1
  import type { DescribeContext, NativeNode } from "../native/node.js";
2
2
  import type { SymbolTheme } from "../symbols.js";
3
3
  import type { Component } from "../tui.js";
4
- /** @internal exported for tests — must stay index-identical to the old regex scan. */
5
- export declare function autolinkSchemeScanIndex(src: string): number | undefined;
6
- /** @internal exported for tests — must never return false for a src the built-in url regex matches. */
7
- export declare function urlTokenPossible(src: string): boolean;
8
4
  /** @internal exported for tests — the grown-line-start block-kind gate. */
9
5
  export declare function fastLineStartHazard(grownLine: string): boolean;
10
6
  /** A hyperlink as the renderer sees it: inline `[text](href)`, `<autolink>`, bare GFM URL, or reference link. */
@@ -20,6 +20,10 @@ import { type Model, type UsageLimit, type UsageReport } from "@oh-my-pi/pi-ai";
20
20
  import { type Component, type TUI } from "../index.js";
21
21
  import { type ModelBrowserSource } from "./model-browser.js";
22
22
  import type { DescribeContext, NativeNode, NativeUiEvent } from "../native/node.js";
23
+ export declare const ADVISOR_REVIEW_MODES: readonly ["turn", "agent-end"];
24
+ export declare const ADVISOR_SYNC_BACKLOG_MODES: readonly ["off", "1", "3", "5", "strict"];
25
+ export type AdvisorReviewMode = (typeof ADVISOR_REVIEW_MODES)[number];
26
+ export type AdvisorSyncBacklog = (typeof ADVISOR_SYNC_BACKLOG_MODES)[number];
23
27
  /** One advisor declared in `WATCHDOG.yml`; its instructions specialize the shared baseline. */
24
28
  export interface AdvisorConfig {
25
29
  name: string;
@@ -32,6 +36,9 @@ export interface AdvisorConfig {
32
36
  enabled?: boolean;
33
37
  /** Maximum non-blocker notes per advisor prompt update (default 4); blockers are exempt. */
34
38
  maxNotesPerUpdate?: number;
39
+ reviewMode?: AdvisorReviewMode;
40
+ reviewInterval?: number;
41
+ syncBacklog?: AdvisorSyncBacklog;
35
42
  }
36
43
  /** Which level a `WATCHDOG.yml` lives at: the project root or the user agent dir. */
37
44
  export type AdvisorConfigScope = "project" | "user";
@@ -92,6 +99,7 @@ export interface AdvisorConfigDeps {
92
99
  thinkingLevel?: ThinkingLevel;
93
100
  }>;
94
101
  availableToolNames: string[];
102
+ syncBacklog?: AdvisorSyncBacklog;
95
103
  /** Formatted advisor-role model shown on the seeded default row (e.g. "anthropic/claude-..."). */
96
104
  defaultModelLabel?: string;
97
105
  }
@@ -88,6 +88,7 @@ export declare class AskDialogComponent implements Component {
88
88
  #private;
89
89
  private readonly callbacks;
90
90
  private readonly options;
91
+ readonly retireDisplacedTranscript = true;
91
92
  constructor(questions: ExtensionAskDialogQuestion[], callbacks: AskDialogCallbacks, options?: AskDialogOptions);
92
93
  invalidate(): void;
93
94
  dispose(): void;
@@ -62,7 +62,9 @@ export interface RuntimeChildrenOptions {
62
62
  * multi-line editor). They are billed at their smallest height so expansion
63
63
  * clips the live tail instead of retiring rows a later shrink could not
64
64
  * reclaim (#11007). Every other root is billed at its current height, so
65
- * settled rows it displaces retire to native scrollback.
65
+ * settled rows it displaces retire to native scrollback. Decision panels
66
+ * declaring `retireDisplacedTranscript` opt out of this floor even when
67
+ * mounted as a child of a transient editor container.
66
68
  */
67
69
  readonly transient?: readonly Component[];
68
70
  /**
@@ -168,6 +168,8 @@ export declare function describeContextUsage(breakdown: ContextBreakdown): Nativ
168
168
  export declare class ContextUsageView extends Container {
169
169
  #private;
170
170
  constructor(breakdown: ContextBreakdown, theme: Theme);
171
+ /** Replace the shown breakdown in place, refreshing both the ANSI and native views. */
172
+ setBreakdown(breakdown: ContextBreakdown): void;
171
173
  describe(cx: DescribeContext): NativeNode;
172
174
  }
173
175
  /**
@@ -61,6 +61,11 @@ export declare class TerminalInfo {
61
61
  * is observed and tests can toggle `Bun.env.ZELLIJ` per case.
62
62
  */
63
63
  export declare function isInsideZellij(env?: NodeJS.ProcessEnv): boolean;
64
+ /**
65
+ * Whether the agent process runs in an SSH session, so the terminal emulator
66
+ * is remote and host-local input/keyboard modes cannot be assumed.
67
+ */
68
+ export declare function isSshSession(env?: NodeJS.ProcessEnv): boolean;
64
69
  export declare function isNotificationSuppressed(): boolean;
65
70
  /**
66
71
  * Whether `PI_FORCE_IMAGE_PROTOCOL` pins the image protocol, including its
@@ -27,6 +27,8 @@ export interface AgentTreeRowOptions {
27
27
  description?: string;
28
28
  preview?: string;
29
29
  stats?: AgentStats;
30
+ /** The agent's own completion estimate (0–100); shown only while live. */
31
+ completionPercent?: number;
30
32
  durationMs?: number;
31
33
  }
32
34
  /** Render an agent row while reserving its identifier and required status badges. */
@@ -255,6 +255,8 @@ export interface AgentProgress {
255
255
  resolvedModelRoute?: string;
256
256
  /** True when a live advisor was attached to this run's session, not merely enabled in settings. */
257
257
  advisor?: boolean;
258
+ /** The agent's latest self-estimate of task completion (0–100), from the periodic `task.completionProbeMs` side request. */
259
+ completionPercent?: number;
258
260
  /** Data extracted by registered subprocess tool handlers (keyed by tool name) */
259
261
  extractedToolData?: Record<string, unknown[]>;
260
262
  /**
@@ -27,6 +27,8 @@ export interface JobSnapshot {
27
27
  resolvedThinkingLevel?: ConfiguredThinkingLevel;
28
28
  /** True when the task progress reports an attached live advisor. */
29
29
  advisor?: boolean;
30
+ /** The task agent's latest self-estimated completion (0–100); see `AgentProgress.completionPercent`. */
31
+ completionPercent?: number;
30
32
  resultText?: string;
31
33
  errorText?: string;
32
34
  /** Source-output metadata retained for per-job warnings and persisted row rendering. */
@@ -28,6 +28,15 @@ interface WriteRenderArgs {
28
28
  file_path?: unknown;
29
29
  content?: unknown;
30
30
  }
31
+ /** A write tool result as the renderer receives it. */
32
+ interface WriteResult {
33
+ content: Array<{
34
+ type: string;
35
+ text?: string;
36
+ }>;
37
+ details?: WriteToolDetails;
38
+ isError?: boolean;
39
+ }
31
40
  declare const writeStreamingPreviewStateKey: unique symbol;
32
41
  /**
33
42
  * Per-component state for incrementally rendering a streamed write.
@@ -66,27 +75,13 @@ export declare const writeToolRenderer: {
66
75
  renderCall(args: WriteRenderArgs, options: RenderResultOptions & WriteStreamingPreviewStateCarrier & {
67
76
  renderContext?: WriteRenderContext;
68
77
  }, uiTheme: Theme): Component | undefined;
69
- renderResult(result: {
70
- content: Array<{
71
- type: string;
72
- text?: string;
73
- }>;
74
- details?: WriteToolDetails;
75
- isError?: boolean;
76
- }, options: RenderResultOptions & {
78
+ renderResult(result: WriteResult, options: RenderResultOptions & {
77
79
  renderContext?: WriteRenderContext;
78
80
  }, uiTheme: Theme, args?: WriteRenderArgs): Component;
79
81
  describeCall(args: WriteRenderArgs, options: RenderResultOptions & {
80
82
  renderContext?: WriteRenderContext;
81
83
  }): NativeToolView | undefined;
82
- describeResult(result: {
83
- content: Array<{
84
- type: string;
85
- text?: string;
86
- }>;
87
- details?: WriteToolDetails;
88
- isError?: boolean;
89
- }, options: RenderResultOptions & {
84
+ describeResult(result: WriteResult, options: RenderResultOptions & {
90
85
  renderContext?: WriteRenderContext;
91
86
  }, args?: WriteRenderArgs): NativeToolView | undefined;
92
87
  mergeCallAndResult: true;
@@ -111,6 +111,9 @@ export interface Component {
111
111
  * last time.
112
112
  */
113
113
  render(width: number): readonly string[];
114
+ /** Inline decision panels must keep displaced settled transcript rows in
115
+ * native scrollback even when mounted inside a transient editor container. */
116
+ readonly retireDisplacedTranscript?: boolean;
114
117
  /**
115
118
  * Describe the component semantically for a Tern Surface Protocol
116
119
  * terminal (see `native/node.ts`). Called instead of `render()` when the
@@ -6,8 +6,11 @@
6
6
  * deliver Shift+Enter as a bare `\r` (indistinguishable from Enter) and
7
7
  * Ctrl+Enter as `\n`. Enabling `CSI ? 9001 h` makes the console host serving
8
8
  * this process report every key as `CSI Vk ; Sc ; Uc ; Kd ; Cs ; Rc _`, which
9
- * carries the full modifier state. Paste text, mouse reports and terminal
10
- * replies keep arriving as plain VT, so only key records need translating.
9
+ * carries the full modifier state. Mouse reports and terminal replies keep
10
+ * arriving as plain VT. Pasted text mostly does too, but the console host
11
+ * re-encodes some pasted characters (line breaks) as key records inside the
12
+ * bracketed paste; {@link Win32InputModeDecoder.decodePaste} turns those back
13
+ * into text.
11
14
  *
12
15
  * Unmodified and legacy-expressible keys translate to the bytes a legacy
13
16
  * terminal sends; chords legacy encoding loses (modified Enter/Tab/Escape,
@@ -23,4 +26,12 @@ export declare class Win32InputModeDecoder {
23
26
  * events it produces — empty for key releases and bare modifier presses.
24
27
  */
25
28
  decode(data: string): string[] | undefined;
29
+ /**
30
+ * Replace the key records embedded in bracketed paste `content` with the
31
+ * text they type, so a pasted line break stays a line break instead of
32
+ * acting as Enter. Key-downs yield their character (`\r` for Enter);
33
+ * releases and keys without text yield nothing. Text that merely looks like
34
+ * a record without ESC is left alone.
35
+ */
36
+ decodePaste(content: string): string;
26
37
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-tui",
4
- "version": "18.4.9",
4
+ "version": "18.4.11",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": {
@@ -41,14 +41,14 @@
41
41
  "gen:glyphs": "bun scripts/gen-glyphs.ts"
42
42
  },
43
43
  "dependencies": {
44
- "@oh-my-pi/omptype": "18.4.9",
45
- "@oh-my-pi/pi-agent-core": "18.4.9",
46
- "@oh-my-pi/pi-ai": "18.4.9",
47
- "@oh-my-pi/pi-catalog": "18.4.9",
48
- "@oh-my-pi/pi-natives": "18.4.9",
49
- "@oh-my-pi/pi-utils": "18.4.9",
50
- "@oh-my-pi/pi-wire": "18.4.9",
51
- "@oh-my-pi/snapcompact": "18.4.9"
44
+ "@oh-my-pi/omptype": "18.4.11",
45
+ "@oh-my-pi/pi-agent-core": "18.4.11",
46
+ "@oh-my-pi/pi-ai": "18.4.11",
47
+ "@oh-my-pi/pi-catalog": "18.4.11",
48
+ "@oh-my-pi/pi-natives": "18.4.11",
49
+ "@oh-my-pi/pi-utils": "18.4.11",
50
+ "@oh-my-pi/pi-wire": "18.4.11",
51
+ "@oh-my-pi/snapcompact": "18.4.11"
52
52
  },
53
53
  "devDependencies": {
54
54
  "kitty-vt-wasm": "^0.2.0"
@@ -110,12 +110,14 @@ function renderAdvisorNote(entry: AdvisorNote, width: number, uiTheme: Theme): s
110
110
  // single ("default") advisor renders unlabeled, as before.
111
111
  const who =
112
112
  entry.advisor && entry.advisor !== "default" ? `${uiTheme.fg("dim", `[${replaceTabs(entry.advisor)}]`)} ` : "";
113
+ const age = entry.turnsAgo !== undefined ? `${uiTheme.fg("dim", `T-${entry.turnsAgo}`)} ` : "";
113
114
  const railGlyph = uiTheme.symbol("advisor.rail");
114
115
  const rail = uiTheme.fg(severityColor(entry.severity), railGlyph);
115
116
  const quoteWidth = visibleWidth(` ${railGlyph} `);
116
117
  const badgeWidth = visibleWidth(badge);
118
+ const ageWidth = visibleWidth(age);
117
119
  const whoWidth = visibleWidth(who);
118
- const w1 = Math.max(10, Math.min(NOTE_LINE_WIDTH, width) - quoteWidth - badgeWidth - whoWidth);
120
+ const w1 = Math.max(10, Math.min(NOTE_LINE_WIDTH, width) - quoteWidth - badgeWidth - ageWidth - whoWidth);
119
121
  const w2 = Math.max(10, Math.min(NOTE_LINE_WIDTH, width) - quoteWidth);
120
122
 
121
123
  const paragraphs = entry.note.split("\n").filter(p => p.trim());
@@ -131,7 +133,7 @@ function renderAdvisorNote(entry: AdvisorNote, width: number, uiTheme: Theme): s
131
133
 
132
134
  return bodyLines.map(
133
135
  (line, index) =>
134
- ` ${rail} ${index === 0 ? `${badge}${who}` : ""}${uiTheme.fg("customMessageText", replaceTabs(line))}`,
136
+ ` ${rail} ${index === 0 ? `${badge}${age}${who}` : ""}${uiTheme.fg("customMessageText", replaceTabs(line))}`,
135
137
  );
136
138
  }
137
139
 
@@ -710,9 +710,10 @@ export class AssistantMessageComponent extends Container {
710
710
  * A `col` (role `omp.assistant`) of `md` nodes keyed by content index, so
711
711
  * streamed deltas reach the terminal as `text append` on the same node;
712
712
  * the tail block carries `stream: true` until the message finalizes.
713
- * Thinking blocks are quiet collapsible `section`s (collapsed per
714
- * `hideThinkingBlock`): a muted "Thinking…" shimmer while live, then
715
- * "Thought for 12s"; tokens and rate ride in the head's `title`.
713
+ * Thinking blocks are quiet collapsible `section`s: a muted "Thinking…"
714
+ * shimmer while live, then "Thought for 12s"; tokens and rate ride in the
715
+ * head's `title`. With `hideThinkingBlock` only the live head shows (collapsed)
716
+ * and a finished thought emits nothing.
716
717
  */
717
718
  override describe(): NativeNode {
718
719
  const tail = this.#displayedMessage ? this.#thinkingTailIndex(this.#displayedMessage) : undefined;
@@ -804,6 +805,8 @@ export class AssistantMessageComponent extends Container {
804
805
  } else if (clock && clock.end === undefined) {
805
806
  clock.end = performance.now();
806
807
  }
808
+ // Hidden (Ctrl+T): only the live "Thinking…" head shows; a finished thought leaves nothing.
809
+ if (this.#hideThinkingBlock && !thinkingLive) continue;
807
810
  const tokens = thinkingLive ? this.#thinkingTokens : (clock?.tokens ?? 0);
808
811
  const title = tokens > 0 ? `${formatNumber(tokens)} tokens` : undefined;
809
812
  // Live: starburst · "Thinking…" · ticking timer · tok/s. Done: "Thought for 12s".
@@ -826,12 +829,7 @@ export class AssistantMessageComponent extends Container {
826
829
  "section",
827
830
  {
828
831
  // `.live` while streaming: the body clamps to its tail under a fade.
829
- // `.ghost` while thinking is hidden (Ctrl+T): only a faint "Thought for 12s" stays.
830
- role: thinkingLive
831
- ? "omp.thinking.live"
832
- : this.#hideThinkingBlock
833
- ? "omp.thinking.ghost"
834
- : "omp.thinking",
832
+ role: thinkingLive ? "omp.thinking.live" : "omp.thinking",
835
833
  collapsible: true,
836
834
  // Open while it streams; a finished thought folds to its "Thought for 12s" line.
837
835
  collapsed: this.#thinkingCollapsed.get(index) ?? (this.#hideThinkingBlock || !thinkingLive),
@@ -205,7 +205,7 @@ export class ChatTranscriptBuilder {
205
205
  const previous = this.#waitingPoll;
206
206
  if (!previous) return;
207
207
  this.#waitingPoll = null;
208
- if (nextToolName === "wait" && previous.isDisplaceableBlock() && this.container.canRemoveBlock(previous)) {
208
+ if (nextToolName === "wait" && previous.isDisplaceableBlock() && this.container.canDisplaceBlock(previous)) {
209
209
  this.container.removeChild(previous);
210
210
  }
211
211
  previous.seal();
@@ -294,6 +294,10 @@ export interface AdvisorNote {
294
294
  severity?: AdvisorSeverity;
295
295
  /** Which configured advisor produced this note (omitted for the default advisor). */
296
296
  advisor?: string;
297
+ /** Primary turns completed when note was emitted. */
298
+ turn?: number;
299
+ /** Primary turns elapsed before merged-batch delivery. */
300
+ turnsAgo?: number;
297
301
  }
298
302
 
299
303
  /** Details payload on the batched `advisor` custom message rendered in the transcript. */
@@ -4,6 +4,7 @@ import { popLoopPhase, pushLoopPhase } from "@oh-my-pi/pi-utils";
4
4
  import { col } from "../native/describe";
5
5
  import type { NativeNode } from "../native/node";
6
6
  import { isNativeSettled, settleNative } from "../native/settle";
7
+ import { isUsageRowBlock } from "../overlays/usage-row";
7
8
  import { isToolActivityComponent } from "./tool-activity";
8
9
 
9
10
  /** Shared animation time supplied by the constrained transcript root. */
@@ -291,6 +292,18 @@ export class TranscriptContainer extends Container {
291
292
  return true;
292
293
  }
293
294
 
295
+ /**
296
+ * Whether a superseded snapshot block (a repeated `wait` poll) may be
297
+ * replaced by its successor. Beyond {@link canRemoveBlock}, a block directly
298
+ * above a usage row stays: that row describes the turn the block belongs to,
299
+ * and removing the block would leave a usage-only turn (#12248).
300
+ */
301
+ canDisplaceBlock(component: Component): boolean {
302
+ if (!this.canRemoveBlock(component)) return false;
303
+ const next = this.children[this.children.indexOf(component) + 1];
304
+ return next === undefined || !isUsageRowBlock(next);
305
+ }
306
+
294
307
  /** Lifecycle state per block in transcript order (diagnostics and tests). */
295
308
  blockStates(): readonly BlockState[] {
296
309
  this.#syncEntries();