@oh-my-pi/pi-tui 18.3.5 → 18.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/types/chat/image-loading.d.ts +24 -0
  3. package/dist/types/chat/transcript-entry.d.ts +9 -0
  4. package/dist/types/chrome/transcript-container.d.ts +9 -2
  5. package/dist/types/overlays/agents-hub.d.ts +7 -4
  6. package/dist/types/overlays/rewind-selector.d.ts +3 -3
  7. package/dist/types/overlays/usage-dashboard.d.ts +9 -2
  8. package/dist/types/prompt/composer-cache.d.ts +34 -16
  9. package/dist/types/prompt/composer.d.ts +15 -18
  10. package/dist/types/prompt/welcome.d.ts +5 -2
  11. package/dist/types/status-line/component.d.ts +13 -4
  12. package/dist/types/status-line/metrics.d.ts +0 -1
  13. package/dist/types/status-line/startup.d.ts +38 -0
  14. package/dist/types/status-line/types.d.ts +0 -2
  15. package/dist/types/terminal-capabilities.d.ts +10 -6
  16. package/dist/types/tools/output-meta.d.ts +2 -1
  17. package/dist/types/tools/vibe.d.ts +4 -0
  18. package/package.json +9 -9
  19. package/src/chat/assistant-message.ts +27 -21
  20. package/src/chat/image-loading.ts +81 -0
  21. package/src/chat/tool-execution.ts +38 -14
  22. package/src/chat/transcript-entry.ts +16 -0
  23. package/src/chrome/transcript-container.ts +208 -74
  24. package/src/components/image.ts +10 -2
  25. package/src/overlays/agents-hub.ts +7 -18
  26. package/src/overlays/copy-selector.ts +3 -31
  27. package/src/overlays/model-hub.ts +49 -16
  28. package/src/overlays/rewind-selector.ts +51 -12
  29. package/src/overlays/usage-dashboard.ts +47 -6
  30. package/src/prompt/composer-cache.ts +217 -196
  31. package/src/prompt/composer.ts +40 -70
  32. package/src/prompt/welcome.ts +31 -28
  33. package/src/render/render-utils.ts +43 -22
  34. package/src/status-line/component.ts +73 -50
  35. package/src/status-line/metrics.ts +3 -21
  36. package/src/status-line/segments.ts +34 -50
  37. package/src/status-line/startup.ts +181 -0
  38. package/src/status-line/types.ts +0 -2
  39. package/src/terminal-capabilities.ts +12 -6
  40. package/src/terminal.ts +38 -9
  41. package/src/tools/output-meta.ts +11 -3
  42. package/src/tools/vibe.ts +15 -2
  43. package/src/tui.ts +66 -42
package/CHANGELOG.md CHANGED
@@ -2,6 +2,40 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.4.1] - 2026-09-28
6
+
7
+ ### Breaking Changes
8
+
9
+ - Replaced `AgentsHubDeps.setDisabledAgents`/`setOverrides` with `setAgentDisabled(name, { disabled })` and `setAgentOverride(property, name, value)`, so each hub edit persists only the agent it changes; `PropertyKind` is exported ([#13308](https://github.com/can1357/oh-my-pi/pull/13308) by [@Vortex727](https://github.com/Vortex727))
10
+
11
+ ### Changed
12
+
13
+ - LSP servers section is omitted from the welcome screen when LSP is disabled
14
+ - Usage dashboards and provider cards can show connected accounts with unavailable usage separately from reported quotas, without treating missing reports as unused or unlimited ([#13476](https://github.com/can1357/oh-my-pi/pull/13476) by [@aktanazat](https://github.com/aktanazat)).
15
+
16
+ ### Fixed
17
+
18
+ - Fixed sessions exiting as if the terminal hung up when it only stopped reading for a few seconds during a very large repaint (a long transcript's replay behind a busy tmux or container attach) ([#13576](https://github.com/can1357/oh-my-pi/pull/13576) by [@sjawhar](https://github.com/sjawhar))
19
+ - Fixed fullscreen overlays rewriting the whole terminal on every change; each frame now repaints only the rows that changed ([#13568](https://github.com/can1357/oh-my-pi/pull/13568) by [@sjawhar](https://github.com/sjawhar))
20
+ - Fixed the armed `/loop` status reading `Loop waiting`, which hid that the next prompt becomes the repeated loop body; it now reads `Loop: next prompt repeats` ([#13435](https://github.com/can1357/oh-my-pi/pull/13435) by [@Dante-dan](https://github.com/Dante-dan)).
21
+ - Reduced memory held by long sessions: committed transcript blocks no longer keep their rendered-row caches ([#13242](https://github.com/can1357/oh-my-pi/pull/13242) by [@iliaal](https://github.com/iliaal)).
22
+ - Fixed the results-limit notice advising `Use limit=` values the tool would clamp straight back; at the hard cap it now reports the reached count alone ([#13263](https://github.com/can1357/oh-my-pi/issues/13263))
23
+ - Fixed the rewind filter finding nothing for query words in scripts without spaces (e.g. Chinese): non-Latin words now match as substrings while Latin words keep whole-word matching ([#13361](https://github.com/can1357/oh-my-pi/pull/13361) by [@jchanghong023](https://github.com/jchanghong023))
24
+ - The Esc-Esc rewind selector opens on the recent tail of long sessions instead of replaying the whole branch; press `a` to load earlier turns ([#12819](https://github.com/can1357/oh-my-pi/pull/12819) by [@lemonleks](https://github.com/lemonleks)).
25
+ - Fixed OSC 8 hyperlinks (file paths, URLs, GitHub refs) rendering as plain text in Herdr panes. Herdr reports `TERM=xterm-256color` without `TERM_PROGRAM`, so its panes were treated as an unknown terminal, although Herdr renders OSC 8 itself and opens links on Ctrl+click. Screen or tmux nested inside a pane still follow their own rules, and `PI_NO_HYPERLINKS=1` still opts out ([#13289](https://github.com/can1357/oh-my-pi/pull/13289) by [@andrebrait](https://github.com/andrebrait)).
26
+ - Fixed native stderr output painting over the TUI on Linux ([#13176](https://github.com/can1357/oh-my-pi/issues/13176)).
27
+ - Fixed heavy lag while output streams with inline images on SIXEL terminals such as Orca: each visible image was re-encoded on every render pass ([#12998](https://github.com/can1357/oh-my-pi/pull/12998) by [@wimjan123](https://github.com/wimjan123))
28
+ - Fixed Kitty-protocol terminals re-encoding every non-PNG image on redelivery and transcript rebuilds ([#13118](https://github.com/can1357/oh-my-pi/issues/13118)).
29
+ - Fixed the Windows Terminal taskbar progress animation restarting every second while the agent works ([#12940](https://github.com/can1357/oh-my-pi/pull/12940)).
30
+ - Fixed resuming a long session freezing the TUI for several seconds; first paint is now spread across frames ([#12933](https://github.com/can1357/oh-my-pi/issues/12933)).
31
+ - The model hub no longer opens a thinking strip or shows the `t` hint for models that cannot reason ([#13112](https://github.com/can1357/oh-my-pi/pull/13112)).
32
+
33
+ ## [18.4.0] - 2026-09-28
34
+
35
+ ### Fixed
36
+
37
+ - Prevented interrupted wait operations from appearing in the transcript when a queued completion or message is received.
38
+
5
39
  ## [18.3.5] - 2026-09-27
6
40
 
7
41
  ### Added
@@ -43,4 +43,28 @@ export declare class InvalidImageDataError extends Error {
43
43
  export declare function imageDecodeFailureReason(image: ImageContent): Promise<string | null>;
44
44
  /** Converts an image to PNG, rejecting when the runtime cannot decode or encode it. */
45
45
  export declare function convertImageToPng(image: ImageContent): Promise<ImageContent>;
46
+ /**
47
+ * Content-addressed identity of an image payload, stable across components and
48
+ * transcript rebuilds. Callers key their own per-image state by this instead of
49
+ * positional ids like `${toolCallId}:${index}`, which go stale when the images
50
+ * behind a position are replaced.
51
+ */
52
+ export declare function imagePayloadKey(image: ImageContent): string;
53
+ /**
54
+ * The PNG conversion of `image` when one is still resident, else `undefined`.
55
+ * Synchronous so renderers can use an already-converted image on the spot
56
+ * instead of scheduling another async re-render.
57
+ */
58
+ export declare function cachedPngConversion(image: ImageContent): ImageContent | undefined;
59
+ /**
60
+ * Converts `image` to PNG at most once per distinct payload: concurrent callers
61
+ * share one in-flight conversion and later callers hit {@link pngCache}.
62
+ * Kitty-graphics renderers use this because `convertImageToPng` is a full
63
+ * decode plus re-encode, and the same image is delivered repeatedly (read-result
64
+ * replay) and rebuilt from scratch on resume, rewind, and `/tree` navigation.
65
+ *
66
+ * Rejections are not cached — a payload that failed to decode is retried by the
67
+ * next caller, matching {@link convertImageToPng}'s contract.
68
+ */
69
+ export declare function convertImageToPngShared(image: ImageContent): Promise<ImageContent>;
46
70
  export declare function ensureSupportedImageInput(image: ImageContent): Promise<ImageContent | null>;
@@ -31,6 +31,15 @@ export declare function transcriptEntryMessage(entry: TranscriptEntryLike): Agen
31
31
  export declare function isUserRequestEntry(entry: TranscriptEntryLike | {
32
32
  type: string;
33
33
  }): boolean;
34
+ /**
35
+ * Recent transcript tail starting at a user-request boundary, so tool calls
36
+ * and results stay together. ChatTranscriptBuilder drops a tool result whose
37
+ * initiating call was sliced away, so a tail of orphaned results can leave
38
+ * the picker without any target.
39
+ *
40
+ * A whole user turn may exceed `limit`; paginate rendering if one turn grows too large.
41
+ */
42
+ export declare function recentTranscriptEntries(entries: TranscriptEntryLike[], limit?: number): TranscriptEntryLike[];
34
43
  /** Editable user request text, preserving skill invocation syntax. */
35
44
  export declare function userTurnDraft(entry: TranscriptEntryLike): string | undefined;
36
45
  /** Extract text blocks verbatim, optionally separating block boundaries. */
@@ -96,8 +96,15 @@ export declare class TranscriptContainer extends Container {
96
96
  * quit is pure write volume. An already offered replay batch stays valid.
97
97
  */
98
98
  cancelReplay(): void;
99
- /** Total rows the live, un-emitted tail occupies at `width`. */
100
- liveRowCount(width: number): number;
99
+ /**
100
+ * Total rows the live, un-emitted tail occupies at `width`.
101
+ *
102
+ * `limit` stops the walk once the total passes it: measuring a resumed
103
+ * session's whole ledger costs one full render per block, and callers only
104
+ * compare the height against a viewport budget. Past `limit` the result is
105
+ * a lower bound, guaranteed only to be greater than `limit`.
106
+ */
107
+ liveRowCount(width: number, limit?: number): number;
101
108
  /** Block spans of the last `renderViewport` output, in output coordinates. Empty when the tail is empty. */
102
109
  getLastViewportSpans(): readonly TranscriptViewportSpan[];
103
110
  /** Render the live tail, constrained to the supplied transcript height. */
@@ -32,7 +32,7 @@ export interface HubAgent {
32
32
  advisorOverride?: string;
33
33
  }
34
34
  /** The per-agent knob a strip or the model browser is editing. */
35
- type PropertyKind = "model" | "prewalk" | "advisor";
35
+ export type PropertyKind = "model" | "prewalk" | "advisor";
36
36
  export interface GeneratedAgentSpec {
37
37
  identifier: string;
38
38
  whenToUse: string;
@@ -47,8 +47,12 @@ export interface AgentsHubDeps {
47
47
  resolvePatterns: (patterns: string[]) => string | undefined;
48
48
  effectivePrewalkPattern: (agent: HubAgent) => string | undefined;
49
49
  effectiveAdvisorPattern: (agent: HubAgent) => string | undefined;
50
- setDisabledAgents: (names: string[]) => void;
51
- setOverrides: (property: PropertyKind, overrides: Record<string, string>) => void;
50
+ /** Persist one agent's enabled state; other agents are untouched. */
51
+ setAgentDisabled: (name: string, options: {
52
+ disabled: boolean;
53
+ }) => void;
54
+ /** Persist one agent's override for `property`; `undefined` clears it. Other agents are untouched. */
55
+ setAgentOverride: (property: PropertyKind, name: string, value: string | undefined) => void;
52
56
  generateAgent: (description: string, onText: (text: string) => void) => Promise<string>;
53
57
  saveAgent: (scope: "project" | "user", spec: GeneratedAgentSpec) => Promise<string>;
54
58
  }
@@ -69,4 +73,3 @@ export declare class AgentsHubComponent implements Component {
69
73
  handleInput(data: string): void;
70
74
  render(width: number): readonly string[];
71
75
  }
72
- export {};
@@ -21,13 +21,13 @@
21
21
  * active column when a strip is open), Left/Right slide between branch
22
22
  * variants at a fork and jump between user turns elsewhere, `f` opens a
23
23
  * filter (typing narrows the current path to matching items; Esc leaves the
24
- * filter with the selection kept), Enter rewinds to the outlined item, Esc
25
- * cancels.
24
+ * filter with the selection kept), Enter rewinds to the outlined item, A loads
25
+ * earlier turns without changing selection, Esc cancels.
26
26
  */
27
27
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
28
28
  import { type Component, type TUI } from "../index.js";
29
29
  import type { MessageRenderer } from "../chat/extension-types.js";
30
- import type { TranscriptEntryLike as TranscriptEntry } from "../chat/transcript-entry.js";
30
+ import { type TranscriptEntryLike as TranscriptEntry } from "../chat/transcript-entry.js";
31
31
  /** One alternate branch at a divergence: its root and message path root → most-recent leaf. */
32
32
  export interface BranchVariantPath {
33
33
  rootId: string;
@@ -20,12 +20,18 @@ export interface CardWindowRow {
20
20
  /** Absolute one-sided amount (e.g. `$12.34 used`, `100 credits left`) for limits without a fraction. */
21
21
  usedText?: string;
22
22
  }
23
+ /** A connected account whose usage lookup produced no attributable report. */
24
+ export interface UnavailableUsageAccount {
25
+ provider: string;
26
+ label: string;
27
+ }
23
28
  /** Compact per-provider summary backing one card in the subscriptions grid. */
24
29
  export interface ProviderCard {
25
30
  provider: string;
26
31
  name: string;
27
- /** Number of accounts reporting for this provider. */
32
+ /** Number of represented accounts, including unavailable usage lookups. */
28
33
  accounts: number;
34
+ unavailableAccounts: string[];
29
35
  /** Window rows sorted most-pressing first. */
30
36
  windows: CardWindowRow[];
31
37
  /** True when every account reports no limits (e.g. enterprise plans). */
@@ -48,7 +54,7 @@ export interface ProviderCard {
48
54
  * most-used account's reset countdown. Cards sort most-pressing first so
49
55
  * what's burning is on top-left; fully idle providers collapse into a tick.
50
56
  */
51
- export declare function buildProviderCards(reports: UsageReport[], nowMs: number): ProviderCard[];
57
+ export declare function buildProviderCards(reports: UsageReport[], nowMs: number, unavailableAccounts?: readonly UnavailableUsageAccount[]): ProviderCard[];
52
58
  /** GitHub-style week-per-column heatmap grid derived from daily activity. */
53
59
  export interface HeatmapLayout {
54
60
  /** Per week column: short month name when the column starts a new month. */
@@ -71,6 +77,7 @@ export declare function buildHeatmapLayout(points: DailyActivityPoint[], weeks:
71
77
  /** Callbacks and data sources for {@link UsageDashboardComponent}. */
72
78
  export interface UsageDashboardOptions {
73
79
  reports: UsageReport[];
80
+ unavailableAccounts?: readonly UnavailableUsageAccount[];
74
81
  /**
75
82
  * Full classic `/usage` report for the expanded detail view; re-invoked per
76
83
  * terminal width.
@@ -1,6 +1,5 @@
1
- import "@oh-my-pi/pi-utils/env";
2
1
  import type { LspServerInfo, RecentSession } from "./welcome.js";
3
- import type { ComposerPreferences, ComposerStatusSnapshot } from "./composer.js";
2
+ import type { ComposerPreferences, ComposerStatusCache } from "./composer.js";
4
3
  import type { SymbolPreset } from "../theme/theme.js";
5
4
  /** Theme inputs cached from the last resolved settings load for stable prepaint colors. */
6
5
  export interface ComposerThemePreferences {
@@ -20,18 +19,37 @@ export interface ComposerStartupCache {
20
19
  readonly theme?: ComposerThemePreferences;
21
20
  readonly welcome?: ComposerWelcomeCache;
22
21
  readonly recentSessions: RecentSession[];
23
- readonly lspServers: LspServerInfo[];
24
- readonly status?: ComposerStatusSnapshot;
22
+ /** `null` when the last run had LSP disabled. */
23
+ readonly lspServers: LspServerInfo[] | null;
24
+ readonly status?: ComposerStatusCache;
25
+ }
26
+ /**
27
+ * Process-wide store at {@link getComposerCacheDbPath}, opened on first use and
28
+ * closed at exit. `undefined` when it cannot be opened (logged once; startup
29
+ * paints without speculation) and under the test runner, so tests never read
30
+ * or clobber the user's cache; tests open {@link ComposerCache.open} explicitly.
31
+ */
32
+ export declare function sharedComposerCache(): ComposerCache | undefined;
33
+ /** SQLite store of the composer state the next launch paints before its session exists. */
34
+ export declare class ComposerCache {
35
+ #private;
36
+ private constructor();
37
+ /**
38
+ * Open (creating if needed) the store at `dbPath`, quarantining a corrupt one once.
39
+ * @throws when the directory or database cannot be created.
40
+ */
41
+ static open(dbPath?: string): ComposerCache;
42
+ /** Everything cached for `cwd`, with any-project rows as fallback for shared kinds. Never throws. */
43
+ read(cwd: string): ComposerStartupCache;
44
+ /** Resolved theme and composer settings for the next prepaint. */
45
+ writeUi(cwd: string, preferences: ComposerPreferences, theme: ComposerThemePreferences): void;
46
+ /** Authoritative model/provider labels for the next welcome prepaint. */
47
+ writeWelcome(cwd: string, welcome: ComposerWelcomeCache): void;
48
+ /** The latest recent-session rows (first four). */
49
+ writeRecentSessions(cwd: string, sessions: readonly RecentSession[]): void;
50
+ /** The latest detected project LSP rows; `null` records that LSP is disabled. */
51
+ writeLspServers(cwd: string, servers: readonly LspServerInfo[] | null): void;
52
+ /** Status-bar inputs for the next prepaint's startup status line. */
53
+ writeStatus(cwd: string, status: ComposerStatusCache): void;
54
+ close(): void;
25
55
  }
26
- /** Read all speculative composer caches synchronously before the first terminal paint. */
27
- export declare function readComposerStartupCache(cwd: string): ComposerStartupCache;
28
- /** Persist resolved theme and composer settings for the next prepaint. */
29
- export declare function writeComposerUiCache(cwd: string, preferences: ComposerPreferences, theme: ComposerThemePreferences): Promise<void>;
30
- /** Persist authoritative model/provider labels for the next welcome prepaint. */
31
- export declare function writeComposerWelcomeCache(cwd: string, welcome: ComposerWelcomeCache): Promise<void>;
32
- /** Persist placeholder-only status chrome for speculative first-frame rendering. */
33
- export declare function writeComposerStatusCache(cwd: string, status: ComposerStatusSnapshot): Promise<void>;
34
- /** Persist the latest recent-session rows as a compact JSONL speculation cache. */
35
- export declare function writeComposerRecentSessionsCache(cwd: string, sessions: readonly RecentSession[]): Promise<void>;
36
- /** Persist the latest detected project LSP rows for the next prepaint. */
37
- export declare function writeComposerLspCache(cwd: string, servers: readonly LspServerInfo[]): Promise<void>;
@@ -1,3 +1,6 @@
1
+ import type { StatusLineComponent } from "../status-line/component.js";
2
+ import type { StatusLineSession } from "../status-line/host.js";
3
+ import { type StatusLineStartupData } from "../status-line/startup.js";
1
4
  import { type Terminal } from "../terminal.js";
2
5
  import { type Component, type ResizeScrollbackMode, type TerminalFramePlan, type TerminalFrameProvider, TUI, type TUIOptions, type ViewportSize } from "../tui.js";
3
6
  import { CustomEditor } from "./custom-editor.js";
@@ -24,27 +27,21 @@ export interface ComposerWelcomeUpdate {
24
27
  readonly modelName?: string;
25
28
  readonly providerName?: string;
26
29
  readonly recentSessions?: readonly RecentSession[];
27
- readonly lspServers?: readonly LspServerInfo[];
30
+ /** Detected project servers; `null` means LSP is disabled and hides the welcome section. */
31
+ readonly lspServers?: readonly LspServerInfo[] | null;
28
32
  }
29
33
  /**
30
- * Placeholder-only status chrome replayed on the next first frame so the
31
- * status band/border exists before the session-aware status line attaches.
32
- * Bound to the composer shape it was rendered for; a different shape drops it.
34
+ * Status-bar inputs persisted by the last session. The first frame renders
35
+ * them through a startup {@link StatusLineComponent} at the live width until
36
+ * the session-aware status line attaches.
33
37
  */
34
- export interface ComposerStatusSnapshot {
35
- readonly shape: string;
36
- /** ANSI wrapper of the editor border at snapshot time (session accent or thinking color). */
38
+ export interface ComposerStatusCache {
39
+ /** ANSI wrapper of the editor border when persisted (session accent or thinking color). */
37
40
  readonly borderColor?: {
38
41
  readonly prefix: string;
39
42
  readonly suffix: string;
40
43
  };
41
- /** Status content embedded in the editor's top chrome (`top-border`, `top-band`, `top-rule-chip`). */
42
- readonly topBorder?: {
43
- readonly content: string;
44
- readonly width: number;
45
- };
46
- /** Standalone bottom-bar rows (`pi`/`claude` shapes), gap row included. */
47
- readonly bottomLines: readonly string[];
44
+ readonly statusLine: StatusLineStartupData;
48
45
  }
49
46
  /** Optional dependencies and initial state for a standalone composer. */
50
47
  export interface ComposerOptions {
@@ -53,7 +50,7 @@ export interface ComposerOptions {
53
50
  readonly tuiOptions?: TUIOptions;
54
51
  readonly preferences?: Partial<ComposerPreferences>;
55
52
  readonly welcome?: ComposerWelcomeUpdate;
56
- readonly status?: ComposerStatusSnapshot;
53
+ readonly status?: ComposerStatusCache;
57
54
  readonly exit?: (code: number) => void;
58
55
  readonly now?: () => number;
59
56
  }
@@ -155,11 +152,11 @@ export declare class Composer implements TerminalFrameProvider {
155
152
  /** Update the canonical editor reference after InteractiveMode remounts a custom editor. */
156
153
  setEditor(editor: CustomEditor): void;
157
154
  /**
158
- * Mount the session-aware status component into the slot below the editor.
159
- * Drops the speculative snapshot; the caller installs the real top-border
155
+ * Mount the session-aware status component into the slot below the editor,
156
+ * retiring the startup status line; the caller installs the real top-border
160
157
  * provider through its composer-shape sync.
161
158
  */
162
- setStatusComponent(component: Component): void;
159
+ setStatusComponent<TSession extends StatusLineSession>(component: StatusLineComponent<TSession>): void;
163
160
  /** Mount or replace session-aware root children while preserving the header and status hosts. */
164
161
  setRuntimeChildren(children: readonly Component[], options?: RuntimeChildrenOptions): void;
165
162
  /** Play or replay the welcome intro against the stable header render target. */
@@ -32,8 +32,11 @@ export declare class WelcomeComponent implements Component {
32
32
  private modelName;
33
33
  private providerName;
34
34
  private recentSessions;
35
+ /** Detected project servers; `null` means LSP is disabled and hides the section. */
35
36
  private lspServers;
36
- constructor(version: string, modelName: string, providerName: string, recentSessions?: RecentSession[], lspServers?: LspServerInfo[]);
37
+ constructor(version: string, modelName: string, providerName: string, recentSessions?: RecentSession[],
38
+ /** Detected project servers; `null` means LSP is disabled and hides the section. */
39
+ lspServers?: LspServerInfo[] | null);
37
40
  get tip(): string | undefined;
38
41
  invalidate(): void;
39
42
  /** The intro keeps the welcome block mutable; settling lets it retire to history. */
@@ -56,7 +59,7 @@ export declare class WelcomeComponent implements Component {
56
59
  setVersion(version: string): void;
57
60
  setModel(modelName: string, providerName: string): void;
58
61
  setRecentSessions(sessions: RecentSession[]): void;
59
- setLspServers(servers: LspServerInfo[]): void;
62
+ setLspServers(servers: LspServerInfo[] | null): void;
60
63
  render(termWidth: number): readonly string[];
61
64
  }
62
65
  /** Block-grid brand mark shared by the welcome and setup surfaces. */
@@ -1,9 +1,9 @@
1
1
  import { type Component, type ComposerStyle } from "../index.js";
2
2
  import type { StatusLineHost, StatusLineSession } from "./host.js";
3
+ import type { Editor } from "../components/editor.js";
3
4
  import { type CodexResetFireworksEvent } from "../overlays/codex-reset-fireworks.js";
4
5
  import { type SegmentContext } from "./segments.js";
5
6
  import type { CollabStatus, EffectiveStatusLineSettings, StatusLineSettings } from "./types.js";
6
- type StatusLineLayout = "box" | "band" | "plain-full" | "plain-left" | "plain-right";
7
7
  export declare class StatusLineComponent<TSession extends StatusLineSession = StatusLineSession> implements Component {
8
8
  #private;
9
9
  private session;
@@ -114,6 +114,12 @@ export declare class StatusLineComponent<TSession extends StatusLineSession = St
114
114
  * never retire the watcher or abort a live resolve.
115
115
  */
116
116
  applyCwdChange(): void;
117
+ /**
118
+ * Seed working-tree status from the status line this one replaces (the
119
+ * startup bar), so the dirty counts stay on screen instead of blanking until
120
+ * this instance's first fetch lands. Normal TTL refreshes follow.
121
+ */
122
+ adoptGitStatus(source: StatusLineComponent): void;
117
123
  /**
118
124
  * Startup redraws only arm a short-delayed task; timeout releases the render
119
125
  * cadence while a late successful fetch can still refresh the cached segment.
@@ -133,8 +139,12 @@ export declare class StatusLineComponent<TSession extends StatusLineSession = St
133
139
  usedTokens: number;
134
140
  contextWindow: number;
135
141
  };
136
- /** Render startup ellipses inside each segment's normal icon, color, and static chrome. */
137
- renderStartupPlaceholder(width: number, layout: StatusLineLayout): string;
142
+ /**
143
+ * Wire this bar into `editor` for a composer layout: the matching top-border
144
+ * provider, the autocomplete probe, and the standalone bottom-bar placement.
145
+ * Callers re-run it whenever the composer shape changes.
146
+ */
147
+ attachToEditor(editor: Pick<Editor, "isAutocompleteActive" | "setTopBorderProvider" | "setTopBorder">, style: Pick<ComposerStyle, "statusAttachment" | "bottomBar" | "bottomBarGap">): void;
138
148
  getTopBorder(width: number, previewTitle?: string): {
139
149
  content: string;
140
150
  width: number;
@@ -181,4 +191,3 @@ export declare class StatusLineComponent<TSession extends StatusLineSession = St
181
191
  getPreviewLines(width: number, style?: Pick<ComposerStyle, "statusAttachment" | "bottomBar">): string[];
182
192
  render(width: number): readonly string[];
183
193
  }
184
- export {};
@@ -5,7 +5,6 @@ export interface BillingSummaryOptions {
5
5
  readonly usingSubscription: boolean;
6
6
  readonly premiumRequests: number;
7
7
  readonly fractionDigits: number;
8
- readonly startupPlaceholder?: boolean;
9
8
  readonly pricingPeriod?: "peak" | "off-peak";
10
9
  readonly advisor?: {
11
10
  readonly cost: number;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Status line for the speculative first frame, before any session exists.
3
+ *
4
+ * InteractiveMode persists {@link StatusLineStartupData} (settings plus the last
5
+ * session's model and thinking state); the next launch renders it through the
6
+ * real {@link StatusLineComponent} as a fresh, unnamed session at the live
7
+ * terminal width. Path and git branch are read live; context usage is reported
8
+ * unknown so the gauge shows the window without a percent.
9
+ *
10
+ * @example
11
+ * ```ts ignore
12
+ * const statusLine = createStartupStatusLine(cached.statusLine);
13
+ * statusLine.attachToEditor(editor, getComposerStyle(shape));
14
+ * ```
15
+ */
16
+ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
17
+ import type { Model } from "@oh-my-pi/pi-catalog/types";
18
+ import { StatusLineComponent } from "./component.js";
19
+ import type { CompactionBoundaries } from "./context-usage.js";
20
+ import type { StatusLineSettings } from "./types.js";
21
+ /** Session-independent inputs that reproduce a fresh session's status bar on the next launch. */
22
+ export interface StatusLineStartupData {
23
+ readonly settings: StatusLineSettings;
24
+ readonly gitEnabled: boolean;
25
+ /** Model of the last session; the next fresh session usually starts on it too. */
26
+ readonly model?: Model;
27
+ readonly thinkingLevel?: ThinkingLevel;
28
+ readonly autoThinking: boolean;
29
+ readonly fastMode: boolean;
30
+ /** Whether `model` bills through a subscription (drives the cost segment's prefix). */
31
+ readonly usingSubscription: boolean;
32
+ readonly autoCompactEnabled: boolean;
33
+ readonly compactionBoundaries: CompactionBoundaries | null;
34
+ }
35
+ /** Build the prepaint status line; the caller disposes it once the session-bound bar mounts. */
36
+ export declare function createStartupStatusLine(data: StatusLineStartupData): StatusLineComponent;
37
+ /** Validate persisted {@link StatusLineStartupData}; `undefined` for anything malformed. */
38
+ export declare function readStatusLineStartupData(value: unknown): StatusLineStartupData | undefined;
@@ -82,8 +82,6 @@ export interface SegmentContext {
82
82
  sessionAccent?: boolean;
83
83
  /** Stand-in session title for previews; `session_name` renders it when the session is unnamed. */
84
84
  previewTitle?: string;
85
- /** Replace dynamic values with ellipses while preserving each segment's icon, color, and static text. */
86
- startupPlaceholder?: boolean;
87
85
  activeRepo: ActiveRepoContext | null;
88
86
  width: number;
89
87
  options: StatusLineSegmentOptions;
@@ -154,11 +154,15 @@ export declare function hyperlinksUserOverride(env?: NodeJS.ProcessEnv): boolean
154
154
  * Policy (highest precedence first):
155
155
  * 1. Explicit user override (`PI_NO_HYPERLINKS=1` off, `PI_FORCE_HYPERLINKS=1`
156
156
  * on). Opt-out wins ties.
157
- * 2. Static terminal capability — terminals whose {@link TerminalInfo} marks
157
+ * 2. Herdr pane with no nested screen/tmux: on. Herdr hides the outer
158
+ * terminal (`TERM=xterm-256color`, no `TERM_PROGRAM`), but it renders
159
+ * OSC 8 in its own grid and opens links itself on Ctrl+click, so the
160
+ * outer terminal's support does not matter.
161
+ * 3. Static terminal capability — terminals whose {@link TerminalInfo} marks
158
162
  * `hyperlinks: false` (e.g. `base`) stay off unless the user forced on.
159
- * 3. GNU screen's explicit session marker (`STY`) always off, even if tmux is
163
+ * 4. GNU screen's explicit session marker (`STY`) always off, even if tmux is
160
164
  * also present: a screen layer anywhere in the path cannot forward OSC 8.
161
- * 4. tmux session (`TMUX` set): enabled when tmux self-reports >= 3.4 via
165
+ * 5. tmux session (`TMUX` set): enabled when tmux self-reports >= 3.4 via
162
166
  * `TERM_PROGRAM_VERSION` (tmux 3.4 stores OSC 8 as a cell attribute and
163
167
  * forwards it to outer terminals whose `terminal-features` include
164
168
  * `hyperlinks`). Older or unknown versions stay off; on outer terminals
@@ -166,11 +170,11 @@ export declare function hyperlinksUserOverride(env?: NodeJS.ProcessEnv): boolean
166
170
  * identical to today. Checked before the screen-family TERM heuristic
167
171
  * because tmux's historical `default-terminal` is `screen-256color`, so
168
172
  * `TERM=screen*` inside a tmux session must NOT short-circuit to off.
169
- * 5. screen-family TERM without `TMUX` always off: screen never gained OSC 8
173
+ * 6. screen-family TERM without `TMUX` always off: screen never gained OSC 8
170
174
  * support.
171
- * 6. tmux-family TERM without `TMUX` env — unusual (e.g. inspection scripts);
175
+ * 7. tmux-family TERM without `TMUX` env — unusual (e.g. inspection scripts);
172
176
  * no version available, so off.
173
- * 7. Otherwise honor the static terminal capability.
177
+ * 8. Otherwise honor the static terminal capability.
174
178
  */
175
179
  export declare function shouldEnableHyperlinksByDefault(env?: NodeJS.ProcessEnv, terminalId?: TerminalId): boolean;
176
180
  /**
@@ -73,9 +73,10 @@ export interface LimitsMeta {
73
73
  reached: number;
74
74
  suggestion: number;
75
75
  };
76
+ /** `suggestion` is omitted when the tool is already at its hard cap, so no larger usable limit exists to advise. */
76
77
  resultLimit?: {
77
78
  reached: number;
78
- suggestion: number;
79
+ suggestion?: number;
79
80
  };
80
81
  headLimit?: {
81
82
  reached: number;
@@ -8,6 +8,8 @@ export interface VibeToolDetails {
8
8
  op: VibeOp;
9
9
  /** Live TV-wall snapshot of the owner's worker sessions at (or during) the call. */
10
10
  screens: VibeScreenSnapshot[];
11
+ /** Sessions the director killed, left off `screens`; shown as a count. */
12
+ hiddenKilled?: string[];
11
13
  spawned?: {
12
14
  id: string;
13
15
  cli: VibeCli;
@@ -38,6 +40,8 @@ export interface VibeScreenSnapshot {
38
40
  id: string;
39
41
  cli: VibeCli;
40
42
  state: VibeSessionState;
43
+ /** Terminated by the director (`vibe_kill`) or mode teardown; unset for workers that died on their own. */
44
+ killed?: boolean;
41
45
  model?: string;
42
46
  turns: number;
43
47
  queued: number;
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.3.5",
4
+ "version": "18.4.1",
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.3.5",
45
- "@oh-my-pi/pi-agent-core": "18.3.5",
46
- "@oh-my-pi/pi-ai": "18.3.5",
47
- "@oh-my-pi/pi-catalog": "18.3.5",
48
- "@oh-my-pi/pi-natives": "18.3.5",
49
- "@oh-my-pi/pi-utils": "18.3.5",
50
- "@oh-my-pi/pi-wire": "18.3.5",
51
- "@oh-my-pi/snapcompact": "18.3.5"
44
+ "@oh-my-pi/omptype": "18.4.1",
45
+ "@oh-my-pi/pi-agent-core": "18.4.1",
46
+ "@oh-my-pi/pi-ai": "18.4.1",
47
+ "@oh-my-pi/pi-catalog": "18.4.1",
48
+ "@oh-my-pi/pi-natives": "18.4.1",
49
+ "@oh-my-pi/pi-utils": "18.4.1",
50
+ "@oh-my-pi/pi-wire": "18.4.1",
51
+ "@oh-my-pi/snapcompact": "18.4.1"
52
52
  },
53
53
  "devDependencies": {
54
54
  "kitty-vt-wasm": "^0.2.0"
@@ -12,7 +12,7 @@ import type { AssistantThinkingRenderer } from "./extension-types";
12
12
  import { ensureThemeSync, getMarkdownTheme, theme } from "../theme";
13
13
  import { EMPTY_LINK_TARGETS, resolveImageOptions } from "../render/render-utils";
14
14
  import { WidthAwareText } from "../render";
15
- import { convertImageToPng } from "./image-loading";
15
+ import { cachedPngConversion, convertImageToPngShared, imagePayloadKey } from "./image-loading";
16
16
  import { canonicalizeMessage, formatThinkingForDisplay, hasDisplayableThinking } from "./thinking-display";
17
17
  import { resolveAssistantErrorPresentation } from "./transcript-render-helpers";
18
18
  import { type CacheInvalidation, CacheInvalidationMarkerComponent } from "./cache-invalidation-marker";
@@ -186,10 +186,20 @@ export class AssistantMessageComponent extends Container {
186
186
  #lastMessage?: AssistantMessage;
187
187
  #emergencyText?: Markdown;
188
188
  #toolImagesByCallId = new Map<string, ImageContent[]>();
189
- #convertedKittyImages = new Map<string, ImageContent>();
189
+ /**
190
+ * Payload keys ({@link imagePayloadKey}) whose Kitty PNG conversion this
191
+ * component already awaits, so a re-delivered image neither re-encodes nor
192
+ * schedules a second {@link updateContent} cascade. The conversions
193
+ * themselves are shared process-wide by {@link convertImageToPngShared}.
194
+ */
195
+ #kittyConversionsAwaited = new Set<string>();
196
+ /**
197
+ * Conversions this component displays, held so a later re-render (theme
198
+ * invalidation) still finds them after the bounded shared cache evicts them.
199
+ */
200
+ #kittyConverted = new Map<string, ImageContent>();
190
201
  #showImages = true;
191
202
  #showToolResultImages = true;
192
- #kittyConversionsInFlight = new Set<string>();
193
203
  #transcriptBlockFinalized: boolean;
194
204
  /** See {@link setMidStreamPublication}; the wire's `stream-revision` axis decides it. */
195
205
  #midStreamPublication = true;
@@ -832,16 +842,6 @@ export class AssistantMessageComponent extends Container {
832
842
  setToolResultImages(toolCallId: string, images: ImageContent[]): void {
833
843
  if (!toolCallId) return;
834
844
  const validImages = images.filter(img => img.type === "image" && img.data && img.mimeType);
835
- for (const key of Array.from(this.#convertedKittyImages.keys())) {
836
- if (key.startsWith(`${toolCallId}:`)) {
837
- this.#convertedKittyImages.delete(key);
838
- }
839
- }
840
- for (const key of Array.from(this.#kittyConversionsInFlight)) {
841
- if (key.startsWith(`${toolCallId}:`)) {
842
- this.#kittyConversionsInFlight.delete(key);
843
- }
844
- }
845
845
  if (validImages.length === 0) {
846
846
  this.#toolImagesByCallId.delete(toolCallId);
847
847
  } else {
@@ -855,21 +855,27 @@ export class AssistantMessageComponent extends Container {
855
855
 
856
856
  #convertImagesForKitty(entries: Array<{ image: ImageContent; key: string }>): void {
857
857
  if (TERMINAL.imageProtocol !== ImageProtocol.Kitty) return;
858
- for (const { image, key } of entries) {
858
+ for (const { image } of entries) {
859
859
  if (image.mimeType === "image/png") continue;
860
- if (this.#convertedKittyImages.has(key) || this.#kittyConversionsInFlight.has(key)) continue;
861
- this.#kittyConversionsInFlight.add(key);
862
- convertImageToPng(image)
860
+ const key = imagePayloadKey(image);
861
+ if (this.#kittyConverted.has(key)) continue;
862
+ const cached = cachedPngConversion(image);
863
+ if (cached) {
864
+ this.#kittyConverted.set(key, cached);
865
+ continue;
866
+ }
867
+ if (this.#kittyConversionsAwaited.has(key)) continue;
868
+ this.#kittyConversionsAwaited.add(key);
869
+ convertImageToPngShared(image)
863
870
  .then(converted => {
864
- this.#kittyConversionsInFlight.delete(key);
865
- this.#convertedKittyImages.set(key, converted);
871
+ this.#kittyConverted.set(key, converted);
866
872
  if (this.#lastMessage) {
867
873
  this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
868
874
  }
869
875
  this.#onImageUpdate?.();
870
876
  })
871
877
  .catch(() => {
872
- this.#kittyConversionsInFlight.delete(key);
878
+ this.#kittyConversionsAwaited.delete(key);
873
879
  });
874
880
  }
875
881
  }
@@ -882,7 +888,7 @@ export class AssistantMessageComponent extends Container {
882
888
  for (const { image, key } of entries) {
883
889
  const displayImage =
884
890
  TERMINAL.imageProtocol === ImageProtocol.Kitty && image.mimeType !== "image/png"
885
- ? this.#convertedKittyImages.get(key)
891
+ ? this.#kittyConverted.get(imagePayloadKey(image))
886
892
  : image;
887
893
  if (TERMINAL.imageProtocol && displayImage) {
888
894
  this.#contentContainer.addChild(