@oh-my-pi/pi-tui 18.4.0 → 18.4.2

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 (65) hide show
  1. package/CHANGELOG.md +42 -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 +24 -3
  5. package/dist/types/overlays/agents-hub.d.ts +7 -4
  6. package/dist/types/overlays/model-browser.d.ts +15 -0
  7. package/dist/types/overlays/rewind-selector.d.ts +4 -3
  8. package/dist/types/overlays/usage-dashboard.d.ts +9 -2
  9. package/dist/types/prompt/composer-attachments.d.ts +7 -0
  10. package/dist/types/prompt/composer-cache.d.ts +34 -16
  11. package/dist/types/prompt/composer.d.ts +15 -18
  12. package/dist/types/prompt/model-mention-autocomplete.d.ts +4 -1
  13. package/dist/types/prompt/welcome.d.ts +5 -2
  14. package/dist/types/render/width-aware-text.d.ts +6 -0
  15. package/dist/types/status-line/component.d.ts +13 -4
  16. package/dist/types/status-line/metrics.d.ts +0 -1
  17. package/dist/types/status-line/startup.d.ts +38 -0
  18. package/dist/types/status-line/types.d.ts +0 -2
  19. package/dist/types/terminal-capabilities.d.ts +10 -6
  20. package/dist/types/terminal.d.ts +7 -0
  21. package/dist/types/theme/color.d.ts +1 -0
  22. package/dist/types/theme/session-color.d.ts +3 -3
  23. package/dist/types/theme/shimmer.d.ts +1 -1
  24. package/dist/types/theme/theme-class.d.ts +1 -1
  25. package/dist/types/tools/output-meta.d.ts +2 -1
  26. package/dist/types/tools/vibe.d.ts +4 -0
  27. package/dist/types/tui.d.ts +3 -2
  28. package/package.json +9 -9
  29. package/src/chat/assistant-message.ts +27 -21
  30. package/src/chat/image-loading.ts +81 -0
  31. package/src/chat/tool-execution.ts +53 -17
  32. package/src/chat/transcript-entry.ts +16 -0
  33. package/src/chrome/transcript-container.ts +282 -86
  34. package/src/components/editor.ts +28 -6
  35. package/src/components/image.ts +25 -9
  36. package/src/components/loader.ts +37 -17
  37. package/src/overlays/agent-hub.ts +16 -6
  38. package/src/overlays/agents-hub.ts +7 -18
  39. package/src/overlays/copy-selector.ts +8 -31
  40. package/src/overlays/model-browser.ts +106 -25
  41. package/src/overlays/model-hub.ts +49 -16
  42. package/src/overlays/rewind-selector.ts +60 -13
  43. package/src/overlays/usage-dashboard.ts +47 -6
  44. package/src/prompt/composer-attachments.ts +54 -7
  45. package/src/prompt/composer-cache.ts +217 -196
  46. package/src/prompt/composer.ts +45 -72
  47. package/src/prompt/custom-editor.ts +24 -6
  48. package/src/prompt/model-mention-autocomplete.ts +22 -10
  49. package/src/prompt/welcome.ts +31 -28
  50. package/src/render/width-aware-text.ts +9 -0
  51. package/src/status-line/component.ts +286 -182
  52. package/src/status-line/metrics.ts +3 -21
  53. package/src/status-line/segments.ts +68 -79
  54. package/src/status-line/startup.ts +181 -0
  55. package/src/status-line/types.ts +0 -2
  56. package/src/terminal-capabilities.ts +12 -6
  57. package/src/terminal.ts +75 -20
  58. package/src/theme/color.ts +15 -0
  59. package/src/theme/session-color.ts +26 -4
  60. package/src/theme/shimmer.ts +55 -76
  61. package/src/theme/theme-class.ts +29 -12
  62. package/src/tools/output-meta.ts +11 -3
  63. package/src/tools/vibe.ts +15 -2
  64. package/src/tui.ts +208 -154
  65. package/src/utils.ts +15 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,48 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.4.2] - 2026-09-28
6
+
7
+ ### Changed
8
+
9
+ - Optimized activity clock rendering by caching timestamp formatting strings
10
+ - Reduced CPU overhead during animated terminal redraws, transcript updates, inline-image bookkeeping, and status-line layout.
11
+ - Reduced per-keystroke CPU in `^` model-mention autocomplete and per-frame CPU while streaming (status line, live transcript blocks, tool cards, frame writes).
12
+
13
+ ### Fixed
14
+
15
+ - Fixed the `@` completion popup swallowing Tab and cursor-movement keys while a narrowed filter matched nothing: with no candidate to accept they now fall through to their normal completion and cursor roles instead of being trapped ([#13046](https://github.com/can1357/oh-my-pi/pull/13046) by [@jchanghong023](https://github.com/jchanghong023)).
16
+ - Fixed the Esc-Esc rewind and fullscreen `/copy` selectors getting stuck at the oldest turn of a long session's recent tail; stepping past it now loads the earlier history
17
+ - Fixed the composer attachment band showing chip `#1` (and other prefix IDs) as still present when only `#10` remained in the prompt ([#13605](https://github.com/can1357/oh-my-pi/issues/13605))
18
+
19
+ ## [18.4.1] - 2026-09-28
20
+
21
+ ### Breaking Changes
22
+
23
+ - 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))
24
+
25
+ ### Changed
26
+
27
+ - LSP servers section is omitted from the welcome screen when LSP is disabled
28
+ - 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)).
29
+
30
+ ### Fixed
31
+
32
+ - 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))
33
+ - 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))
34
+ - 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)).
35
+ - 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)).
36
+ - 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))
37
+ - 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))
38
+ - 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)).
39
+ - 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)).
40
+ - Fixed native stderr output painting over the TUI on Linux ([#13176](https://github.com/can1357/oh-my-pi/issues/13176)).
41
+ - 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))
42
+ - 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)).
43
+ - Fixed the Windows Terminal taskbar progress animation restarting every second while the agent works ([#12940](https://github.com/can1357/oh-my-pi/pull/12940)).
44
+ - 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)).
45
+ - 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)).
46
+
5
47
  ## [18.4.0] - 2026-09-28
6
48
 
7
49
  ### Fixed
@@ -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,11 +96,32 @@ 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
+ * Open one composed frame: every live-block measurement until this frame's
101
+ * {@link renderViewport} returns renders against `frame` and is taken once,
102
+ * so the retirement peek and the viewport share each block's render.
103
+ *
104
+ * Callers MUST call {@link renderViewport} with the same `frame` in the same
105
+ * synchronous composition, without mutating any transcript block in
106
+ * between: the shared rows are only as fresh as that first measurement.
107
+ */
108
+ beginFrame(frame: AnimationFrame): void;
109
+ /**
110
+ * Total rows the live, un-emitted tail occupies at `width`.
111
+ *
112
+ * `limit` stops the walk once the total passes it: measuring a resumed
113
+ * session's whole ledger costs one full render per block, and callers only
114
+ * compare the height against a viewport budget. Past `limit` the result is
115
+ * a lower bound, guaranteed only to be greater than `limit`.
116
+ */
117
+ liveRowCount(width: number, limit?: number): number;
101
118
  /** Block spans of the last `renderViewport` output, in output coordinates. Empty when the tail is empty. */
102
119
  getLastViewportSpans(): readonly TranscriptViewportSpan[];
103
- /** Render the live tail, constrained to the supplied transcript height. */
120
+ /**
121
+ * Render the live tail, constrained to the supplied transcript height.
122
+ * Closes the frame {@link beginFrame} opened; a different `frame` discards
123
+ * its measurements first.
124
+ */
104
125
  renderViewport(width: number, rows: number, frame: AnimationFrame): readonly string[];
105
126
  /** Offers stable-head emission or the shortest finalized prefix needed under pressure. */
106
127
  peekFinalizedBatch(width: number, capacity: number): HistoryBatch | undefined;
@@ -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 {};
@@ -37,6 +37,11 @@ export interface ResolvedModelRoleValue {
37
37
  }
38
38
  /** Host-provided preferences and model-role resolution for the browser. */
39
39
  export interface ModelBrowserSource extends ModelRoleLookup {
40
+ /**
41
+ * Changes whenever any preference this source reads or resolves against changes,
42
+ * except the storage-backed `mruOrder` and `modelPerf`. Keys derived-scope caches.
43
+ */
44
+ readonly revision: number;
40
45
  readonly defaultThinkingLevel: string;
41
46
  readonly modelProviderOrder: readonly string[];
42
47
  readonly knownRoleIds: readonly string[];
@@ -104,6 +109,16 @@ export interface SessionModelScope {
104
109
  }
105
110
  /** Build the session picker's current scope without creating an interactive browser. */
106
111
  export declare function buildSessionModelScope(settings: ModelBrowserSource, registry: ModelBrowserRegistry, scopedModels: ReadonlyArray<Model>): SessionModelScope;
112
+ /**
113
+ * {@link buildSessionModelScope} for per-keystroke callers: returns the same
114
+ * scope until the source revision, MRU order, scoped models, or the registry's
115
+ * available models, catalog, or load error change.
116
+ */
117
+ export declare class SessionModelScopeCache {
118
+ #private;
119
+ constructor(settings: ModelBrowserSource, registry: ModelBrowserRegistry);
120
+ get(scopedModels: ReadonlyArray<Model>): SessionModelScope;
121
+ }
107
122
  /** User affinity used to order search matches within one relevance tier. */
108
123
  export interface SearchAffinity {
109
124
  /** `provider/id` (lowercased) → rank; configured-role models first, then MRU. */
@@ -21,13 +21,14 @@
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 (stepping above the oldest replayed
26
+ * turn loads them too), Esc cancels.
26
27
  */
27
28
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
28
29
  import { type Component, type TUI } from "../index.js";
29
30
  import type { MessageRenderer } from "../chat/extension-types.js";
30
- import type { TranscriptEntryLike as TranscriptEntry } from "../chat/transcript-entry.js";
31
+ import { type TranscriptEntryLike as TranscriptEntry } from "../chat/transcript-entry.js";
31
32
  /** One alternate branch at a divergence: its root and message path root → most-recent leaf. */
32
33
  export interface BranchVariantPath {
33
34
  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.
@@ -37,6 +37,13 @@ export declare function attachmentSgr(kind: ChipKind, n: number): string;
37
37
  export declare const PLACEHOLDER_REGEX: RegExp;
38
38
  /** Matches an expanded attachment marker, a compact attachment chip, or a skill chip. */
39
39
  export declare const COMPOSER_TOKEN_REGEX: RegExp;
40
+ /**
41
+ * Attachment indices referenced by a composer buffer, per kind. Image/video count compact chips
42
+ * and expanded markers; paste counts compact chips only, since `[Paste #N]` markers number the
43
+ * base editor's separate paste buffer. Registered labels take precedence over theme glyphs so
44
+ * tokens created before a theme switch keep their kind; full numbers prevent `#1` matching `#10`.
45
+ */
46
+ export declare function referencedAttachments(text: string, recorded: ReadonlyMap<string, ChipKind>): Record<ChipKind, Set<number>>;
40
47
  /** Add the registered model-chip labels to the composer placeholder matcher. */
41
48
  export declare function composerTokenRegex(mentionLabels: Iterable<string>): RegExp;
42
49
  /** Offsets image marker indices, including matching `attachment://` references. */
@@ -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. */
@@ -17,7 +17,10 @@ export declare function applyModelMentionCompletion(lines: string[], cursorLine:
17
17
  cursorLine: number;
18
18
  cursorCol: number;
19
19
  };
20
- /** Create a fresh session-scoped model candidate lookup using picker ordering. */
20
+ /**
21
+ * Create a session-scoped model candidate lookup using picker ordering. The
22
+ * scope and search affinity are reused across queries until their inputs change.
23
+ */
21
24
  export declare function createModelMentionSource(host: {
22
25
  source: ModelBrowserSource;
23
26
  registry: ModelBrowserRegistry;
@@ -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. */
@@ -19,5 +19,11 @@ export declare class WidthAwareText implements Component {
19
19
  setCustomBgFn(customBgFn?: (text: string) => string): void;
20
20
  setIgnoreTight(ignore: boolean): this;
21
21
  invalidate(): void;
22
+ /**
23
+ * Re-run the formatter on the next render because its inputs changed.
24
+ * Unlike {@link invalidate}, the inner `Text` keeps its wrap cache, so a
25
+ * reformat that yields the same string skips re-wrapping and re-tinting.
26
+ */
27
+ reformat(): void;
22
28
  render(width: number): readonly string[];
23
29
  }
@@ -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
  /**
@@ -276,6 +276,13 @@ export declare class ProcessTerminal implements Terminal {
276
276
  stop(): void;
277
277
  write(data: string): void;
278
278
  get columns(): number;
279
+ /**
280
+ * With the output pump, a backlog bound at or below
281
+ * {@link STDOUT_BACKLOG_CLEAR_BYTES} is reported as-is instead of re-read:
282
+ * both consumers (the render gate and the stall watchdog) act only on a
283
+ * backlog above that level, so the bound already decides them and spares a
284
+ * native read on every frame.
285
+ */
279
286
  get pendingOutputBytes(): number;
280
287
  get hostOwnsGridOnResize(): boolean;
281
288
  get rows(): number;
@@ -5,6 +5,7 @@ export declare const FG_RESET = "\u001B[39m";
5
5
  export declare const BG_RESET = "\u001B[49m";
6
6
  /** Resolve theme color depth from the shared terminal capability model. */
7
7
  export declare function detectColorMode(env?: NodeJS.ProcessEnv): ColorMode;
8
+ /** Convert a theme color to foreground SGR at the requested depth; throws for invalid colors. */
8
9
  export declare function colorToAnsi(color: string, mode: ColorMode): string;
9
10
  export declare function fgAnsi(color: string | number, mode: ColorMode): string;
10
11
  export declare function bgAnsi(color: string | number, mode: ColorMode): string;
@@ -1,11 +1,11 @@
1
1
  /** Theme-derived inputs for {@link getSessionAccentHex}; see `Theme.sessionAccentInputs`. */
2
2
  export interface SessionAccentTheme {
3
3
  /** Theme accent hex; the session accent adopts its OKLCH lightness and chroma. */
4
- accentHex: string;
4
+ readonly accentHex: string;
5
5
  /** Major theme color hexes checked for hue collision. */
6
- colorHexes: string[];
6
+ readonly colorHexes: readonly string[];
7
7
  /** WCAG luminance of the status-line surface on light themes; undefined on dark themes. */
8
- surfaceLuminance?: number;
8
+ readonly surfaceLuminance?: number;
9
9
  }
10
10
  /**
11
11
  * Derive a stable CSS hex accent color from a session name and the active theme.
@@ -1,6 +1,6 @@
1
1
  import type { Theme, ThemeColor } from "./theme.js";
2
2
  type ShimmerTheme = Pick<Theme, "bold" | "fg" | "getFgAnsi">;
3
- /** Sweep style for animated shimmer text; `disabled` renders every tier as the low color. */
3
+ /** Sweep style for animated shimmer text; `disabled` renders every tier as the mid color. */
4
4
  export type ShimmerMode = "classic" | "kitt" | "disabled";
5
5
  /** Select the shimmer sweep style. The host pushes its `display.shimmer` preference here. */
6
6
  export declare function setShimmerMode(mode: ShimmerMode): void;