@oh-my-pi/pi-tui 18.4.12 → 18.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,37 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.5.0] - 2026-10-03
6
+
7
+ ### Breaking Changes
8
+
9
+ - `WelcomeComponent` no longer takes a model or provider: its constructor is `(version)`, and `setModel()`, `setRecentSessions()`, `setLspServers()`, `handleNativeEvent()`, `RecentSession`, `LspServerInfo`, `WELCOME_SESSION_SLOTS` and `WELCOME_LSP_SLOTS` are gone; `ComposerWelcomeUpdate` drops `modelName`/`providerName`/`recentSessions`/`lspServers`, and `ComposerCache` drops `writeWelcome()`, `writeRecentSessions()`, `writeLspServers()`, `ComposerWelcomeCache` and the `welcome`/`recentSessions`/`lspServers` fields of `ComposerStartupCache`.
10
+ - `renderWelcomeTip()` returns its lines without an indent, `Tip:` and the body wrapped together, for the caller to place.
11
+
12
+ ### Added
13
+
14
+ - Added `ReportPanel`, a read-only command report: a `/btw`-style titled box with an Esc hint in text mode (above the editor, or as a full-screen page whose body scrolls on the arrow/page/Home/End keys and the wheel), and natively a `/usage`-style sheet whose body the terminal scrolls once it is long, with a Close button ([#14136](https://github.com/can1357/oh-my-pi/pull/14136) by [@H4vC](https://github.com/H4vC)).
15
+ - Added `contextUsageHead()`, the `/context` title naming the model and its window ([#14136](https://github.com/can1357/oh-my-pi/pull/14136) by [@H4vC](https://github.com/H4vC)).
16
+ - Added `Composer.rowsBelow()`, the rows the chrome under a below-transcript root took in the last frame ([#14136](https://github.com/can1357/oh-my-pi/pull/14136) by [@H4vC](https://github.com/H4vC)).
17
+ - Added `Composer.pinInputToBottom()`, which keeps the input on the bottom row after chrome above the editor closes when rows it displaced went to scrollback ([#14136](https://github.com/can1357/oh-my-pi/pull/14136) by [@H4vC](https://github.com/H4vC)).
18
+ - Added a repeatable native `reveal` (`{ at, n }`): a described node scrolls into view again whenever its `n` changes, without being re-added.
19
+
20
+ ### Changed
21
+
22
+ - `ContextUsageView` is now a bare report body without its own title, rules or card; `setBreakdown()` was removed ([#14136](https://github.com/can1357/oh-my-pi/pull/14136) by [@H4vC](https://github.com/H4vC)).
23
+ - The terminal welcome banner is the gradient logo beside the `omp` wordmark with the version under it and the tip, centered in the terminal; the logo stands alone when the lockup does not fit, and the tip drops below 50 columns. It no longer greets with "Welcome back!", natively either.
24
+
25
+ ### Fixed
26
+
27
+ - Fixed tool previews on Windows showing a working directory on another drive as a raw absolute path instead of its home-shortened `~/…` form.
28
+ - Fixed pasted drive-less `file:///…` URLs (forwarded from a macOS pasteboard or remote session) staying undecoded on Windows instead of loading as image paths.
29
+ - Fixed an output artifact whose file cannot be opened (e.g. a directory in the way) being reported on Windows as a write failure and retried later, instead of a terminal open failure.
30
+ - Fixed plan review's Contents in Tern: clicking an entry, or moving through them with the arrow keys, now scrolls the plan to that section.
31
+
32
+ ### Removed
33
+
34
+ - Removed `JobsPanel`, the `/jobs` transcript block; `/jobs` now opens `JobsSheet` natively ([#14138](https://github.com/can1357/oh-my-pi/pull/14138) by [@H4vC](https://github.com/H4vC)).
35
+
5
36
  ## [18.4.12] - 2026-10-02
6
37
 
7
38
  ### Changed
@@ -28,10 +28,12 @@ export type NativeNode = {
28
28
  */
29
29
  readonly key?: string;
30
30
  /**
31
- * Scroll the node into view, placed like the `reveal` op, when it is
32
- * added. Key a node by what it points at to reveal it again on a move.
31
+ * Scroll the node into view, placed like the `reveal` op. A placement
32
+ * reveals it when it is added: key a node by what it points at to
33
+ * reveal it again on a move. A {@link NativeReveal} reveals it whenever
34
+ * its `n` differs from the previous description of the same node.
33
35
  */
34
- readonly reveal?: "start" | "end" | "nearest";
36
+ readonly reveal?: NativeRevealAt | NativeReveal;
35
37
  /**
36
38
  * Keyboard scrolling forwarded to the terminal (PgUp/PgDn/End reach the
37
39
  * program): moves the scroller at or above the node by `by` whenever
@@ -42,6 +44,17 @@ export type NativeNode = {
42
44
  readonly scroll?: NativeScroll;
43
45
  };
44
46
  }[TspKind];
47
+ /** Where a revealed node lands in its scroller. */
48
+ export type NativeRevealAt = "start" | "end" | "nearest";
49
+ /**
50
+ * A repeatable {@link NativeNode.reveal}: bump `n` to bring an existing node
51
+ * into view again (a Contents entry jumping to its section). A freshly added
52
+ * node is not revealed.
53
+ */
54
+ export interface NativeReveal {
55
+ readonly at: NativeRevealAt;
56
+ readonly n: number;
57
+ }
45
58
  /** A {@link NativeNode.scroll} request. */
46
59
  export interface NativeScroll {
47
60
  readonly by: TspScrollBy;
@@ -1,4 +1,4 @@
1
- import { type Component, Container } from "../tui.js";
1
+ import type { Component } from "../tui.js";
2
2
  import type { DescribeContext, NativeNode, NativeUiEvent } from "../native/node.js";
3
3
  /** One async job as the session snapshots it. */
4
4
  export interface JobsPanelJob {
@@ -40,12 +40,6 @@ export interface JobsSheetSource {
40
40
  /** Dismiss the sheet. */
41
41
  close(): void;
42
42
  }
43
- /** The `/jobs` block: `ansi` renders in the terminal, the snapshot describes the native frame. */
44
- export declare class JobsPanel extends Container {
45
- #private;
46
- constructor(snapshot: JobsPanelSnapshot, nowMs: number, ansi: readonly Component[]);
47
- describe(cx: DescribeContext): NativeNode;
48
- }
49
43
  /**
50
44
  * The jobs pill's sheet: a centred `lg` glass sheet titled Background jobs.
51
45
  * A selectable list of every job (running first) sits over the selected
@@ -0,0 +1,68 @@
1
+ import type { TspText } from "@oh-my-pi/pi-wire";
2
+ import { OverlayPanel } from "../chrome/overlay-box.js";
3
+ import type { NativeNode, NativeUiEvent } from "../native/node.js";
4
+ import type { Component } from "../tui.js";
5
+ /** Options of a {@link ReportPanel}. */
6
+ export interface ReportPanelOptions {
7
+ /** Plain title: the text-mode border title, and the native sheet head unless {@link head} is given. */
8
+ readonly title: string;
9
+ /** Styled native sheet head. */
10
+ readonly head?: TspText;
11
+ /** The read-only report; it draws no frame or title of its own. */
12
+ readonly body: Component;
13
+ /**
14
+ * Display form of the key that dismisses the report while it sits above the
15
+ * editor (the editor's interrupt key, which routes to it). Once the panel
16
+ * holds focus ({@link ReportPanel.holdFocus}) the hint shows the key its own
17
+ * input closes on instead.
18
+ */
19
+ readonly closeKey: string;
20
+ /** The focused panel (native sheet, text-mode full-screen page) closes on Esc / its Close button: this runs. */
21
+ readonly onClose: () => void;
22
+ /**
23
+ * Most rows the text-mode panel may take, borders included (the screen rows
24
+ * left above the editor); a taller report scrolls inside it. `undefined`
25
+ * leaves it uncapped.
26
+ */
27
+ readonly maxRows?: () => number | undefined;
28
+ }
29
+ /**
30
+ * A read-only command report (`/changelog`, `/context`, `/tools`, …).
31
+ *
32
+ * Text mode draws a titled box with the body and an Esc hint: above the editor
33
+ * like the `/btw` panel while it fits there (the editor keeps focus and its
34
+ * Esc takes it away), or as a full-screen page on the alternate screen when it
35
+ * is taller, whose body scrolls on the arrow/page/Home/End keys and the wheel.
36
+ * Natively it is a sheet like `/usage`: a centred `lg` glass sheet titled by
37
+ * the report whose body the terminal scrolls (wheel, and the same keys
38
+ * forwarded as scroll requests), then a Close button; Esc closes it.
39
+ */
40
+ export declare class ReportPanel extends OverlayPanel {
41
+ #private;
42
+ readonly nativeOverlay: NonNullable<Component["nativeOverlay"]>;
43
+ constructor(options: ReportPanelOptions);
44
+ /**
45
+ * The panel is shown with focus (native sheet, text-mode full-screen page):
46
+ * its own {@link handleInput} closes it, so the hint names that key.
47
+ */
48
+ holdFocus(): void;
49
+ /**
50
+ * Text mode: cap the box to {@link ReportPanelOptions.maxRows}, scrolling a
51
+ * taller body. The cap is a hard limit: when a resize leaves fewer rows than
52
+ * the box's chrome needs, its bottom (the hint and closing border) is cut
53
+ * rather than spilling over the editor.
54
+ */
55
+ render(width: number): readonly string[];
56
+ /** Text mode: rows the whole box takes at `width` when nothing caps it. */
57
+ heightAt(width: number): number;
58
+ /** The sheet body (the overlay is the frame, and its body the scroller): the report, then Close (Esc). */
59
+ describe(): NativeNode;
60
+ invalidate(): void;
61
+ /**
62
+ * Focused (the native sheet, or the text-mode full-screen page): Esc closes;
63
+ * the arrow, page, Home and End keys and the wheel scroll the body.
64
+ */
65
+ handleInput(data: string): void;
66
+ /** Close runs what Esc runs. */
67
+ handleNativeEvent(event: NativeUiEvent): void;
68
+ }
@@ -1,4 +1,4 @@
1
- import type { Effort } from "@oh-my-pi/pi-ai";
1
+ import { type ConfiguredThinkingLevel } from "../thinking.js";
2
2
  import { SelectList, type SgrMouseEvent } from "../index.js";
3
3
  import { OverlayPanel } from "../chrome/overlay-box.js";
4
4
  import type { DescribeContext, NativeNode, NativeUiEvent } from "../native/node.js";
@@ -7,7 +7,7 @@ import type { DescribeContext, NativeNode, NativeUiEvent } from "../native/node.
7
7
  */
8
8
  export declare class ThinkingSelectorComponent extends OverlayPanel {
9
9
  #private;
10
- constructor(currentLevel: Effort, availableLevels: Effort[], onSelect: (level: Effort) => void, onCancel: () => void);
10
+ constructor(currentLevel: ConfiguredThinkingLevel | undefined, availableLevels: ConfiguredThinkingLevel[], onSelect: (level: ConfiguredThinkingLevel) => void, onCancel: () => void);
11
11
  describe(cx: DescribeContext): NativeNode | null;
12
12
  /** Picker pointer events drive the list exactly as its keys do. */
13
13
  handleNativeEvent(event: NativeUiEvent): void;
@@ -1,4 +1,3 @@
1
- import type { LspServerInfo, RecentSession } from "./welcome.js";
2
1
  import type { ComposerPreferences, ComposerStatusCache } from "./composer.js";
3
2
  import type { SymbolPreset } from "../theme/theme.js";
4
3
  /** Theme inputs cached from the last resolved settings load for stable prepaint colors. */
@@ -8,19 +7,10 @@ export interface ComposerThemePreferences {
8
7
  readonly darkTheme?: string;
9
8
  readonly lightTheme?: string;
10
9
  }
11
- /** Last authoritative model labels shown in the welcome component. */
12
- export interface ComposerWelcomeCache {
13
- readonly modelName: string;
14
- readonly providerName: string;
15
- }
16
10
  /** Speculative composer state read before the settings/session graph is available. */
17
11
  export interface ComposerStartupCache {
18
12
  readonly preferences?: ComposerPreferences;
19
13
  readonly theme?: ComposerThemePreferences;
20
- readonly welcome?: ComposerWelcomeCache;
21
- readonly recentSessions: RecentSession[];
22
- /** `null` when the last run had LSP disabled. */
23
- readonly lspServers: LspServerInfo[] | null;
24
14
  readonly status?: ComposerStatusCache;
25
15
  }
26
16
  /**
@@ -43,12 +33,6 @@ export declare class ComposerCache {
43
33
  read(cwd: string): ComposerStartupCache;
44
34
  /** Resolved theme and composer settings for the next prepaint. */
45
35
  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
36
  /** Status-bar inputs for the next prepaint's startup status line. */
53
37
  writeStatus(cwd: string, status: ComposerStatusCache): void;
54
38
  close(): void;
@@ -6,7 +6,7 @@ import { type Component, type ResizeScrollbackMode, type TerminalFramePlan, type
6
6
  import type { NativeSurface, NativeSurfaceProvider } from "../native/node.js";
7
7
  import { CustomEditor } from "./custom-editor.js";
8
8
  import type { WordCompletionMethod } from "./word-completion.js";
9
- import { type LspServerInfo, type RecentSession, WelcomeComponent } from "./welcome.js";
9
+ import { WelcomeComponent } from "./welcome.js";
10
10
  /** Live settings that affect the composer before and after session adoption. */
11
11
  export interface ComposerPreferences {
12
12
  readonly quiet: boolean;
@@ -25,11 +25,6 @@ export declare const COMPOSER_DEFAULTS: ComposerPreferences;
25
25
  /** Welcome data that can be supplied initially or patched as startup resolves it. */
26
26
  export interface ComposerWelcomeUpdate {
27
27
  readonly version?: string;
28
- readonly modelName?: string;
29
- readonly providerName?: string;
30
- readonly recentSessions?: readonly RecentSession[];
31
- /** Detected project servers; `null` means LSP is disabled and hides the welcome section. */
32
- readonly lspServers?: readonly LspServerInfo[] | null;
33
28
  }
34
29
  /**
35
30
  * Status-bar inputs persisted by the last session. The first frame renders
@@ -122,6 +117,21 @@ export declare class Composer implements TerminalFrameProvider, NativeSurfacePro
122
117
  #private;
123
118
  readonly ui: TUI;
124
119
  constructor(options?: ComposerOptions);
120
+ /**
121
+ * Rows the below-transcript chrome under `root` (editor, status line, …)
122
+ * took in the last frame, so a root that grows upward can cap itself to
123
+ * the screen rows left above them; `undefined` before `root` was laid out.
124
+ */
125
+ rowsBelow(root: Component): number | undefined;
126
+ /**
127
+ * Keep the input on the bottom row while the live rows cannot fill the
128
+ * screen, as after an inline decision panel closes. A tall block that just
129
+ * left the chrome above the editor (a command report) may have scrolled
130
+ * rows into native history that cannot be pulled back; without the pin the
131
+ * editor would jump up to where the shorter frame now ends. The pin lifts
132
+ * once live rows fill the screen again.
133
+ */
134
+ pinInputToBottom(): void;
125
135
  /** Compose the bounded mutable viewport and the next ordered history append. */
126
136
  renderFrame(viewport: ViewportSize): TerminalFramePlan;
127
137
  /**
@@ -164,7 +174,7 @@ export declare class Composer implements TerminalFrameProvider, NativeSurfacePro
164
174
  enableInput(): void;
165
175
  /** Apply settings changes without replacing the editor or welcome component. */
166
176
  setPreferences(update: Partial<ComposerPreferences>): void;
167
- /** Patch welcome data in place as model, session, and project discovery complete. */
177
+ /** Patch welcome data in place as version, session, and project discovery complete. */
168
178
  updateWelcome(update: ComposerWelcomeUpdate): void;
169
179
  /** Replace optional header content around the stable welcome scene. */
170
180
  setHeaderExtras(before: readonly Component[], after: readonly Component[]): void;
@@ -1,58 +1,36 @@
1
- import type { DescribeContext, NativeNode, NativeUiEvent } from "../native/node.js";
1
+ import type { DescribeContext, NativeNode } from "../native/node.js";
2
2
  import type { Component } from "../tui.js";
3
- /**
4
- * Fixed number of session rows in the welcome box so its height stays stable
5
- * across recent-session updates.
6
- */
7
- export declare const WELCOME_SESSION_SLOTS = 4;
8
- /**
9
- * Fixed number of LSP-server rows, for the same reason. Overflow is sliced so
10
- * the box height is constant regardless of how many servers a project has.
11
- */
12
- export declare const WELCOME_LSP_SLOTS = 4;
13
3
  /** Pick a tip from `tips`, biased toward "[NEW]" tips by {@link NEW_TIP_WEIGHT};
14
4
  * `r` is a uniform sample in [0, 1). Returns "" when `tips` is empty.
15
5
  * Exported for tests. */
16
6
  export declare function pickWeightedTip(tips: readonly string[], r: number): string;
17
- export declare function renderWelcomeTip(tip: string, boxWidth: number, phase?: number): string[];
18
- export interface RecentSession {
19
- name: string;
20
- timeAgo: string;
21
- /** Session file; a native click on the row resumes it. */
22
- path?: string;
23
- }
24
- export interface LspServerInfo {
25
- name: string;
26
- status: "ready" | "error" | "connecting" | "available";
27
- fileTypes: string[];
28
- }
29
7
  /**
30
- * Premium welcome screen with block-based OMP logo and two-column layout.
8
+ * The welcome tip as lines of at most `width` columns: `Tip:` and the body
9
+ * wrapped together, with no indent, so the banner can center each line.
10
+ * `[]` when `width` leaves no room for a useful line.
11
+ */
12
+ export declare function renderWelcomeTip(tip: string, width: number, phase?: number): string[];
13
+ /**
14
+ * The session's welcome banner. In a terminal: the gradient logo beside the
15
+ * `omp` wordmark with the version under it (the logo alone when the lockup does
16
+ * not fit) and the tip of the session (dropped below {@link TIP_MIN_COLUMNS}
17
+ * columns). Natively: a card with the same logo, wordmark, version and tip
18
+ * ({@link WelcomeComponent.describe}).
31
19
  */
32
20
  export declare class WelcomeComponent implements Component {
33
21
  #private;
34
22
  private version;
35
- private modelName;
36
- private providerName;
37
- private recentSessions;
38
- /** Detected project servers; `null` means LSP is disabled and hides the section. */
39
- private lspServers;
40
- constructor(version: string, modelName: string, providerName: string, recentSessions?: RecentSession[],
41
- /** Detected project servers; `null` means LSP is disabled and hides the section. */
42
- lspServers?: LspServerInfo[] | null);
23
+ constructor(version: string);
43
24
  get tip(): string | undefined;
44
25
  invalidate(): void;
45
26
  /**
46
- * A `card` (`omp.welcome`) titled with the app version. The brand column
47
- * (`omp.welcome.brand`: greeting and the terminal's builtin `omp` mark, which
48
- * it animates) sits beside the info column (`omp.welcome.info`: prompt-sigil
49
- * keycaps, LSP servers, recent sessions); the tip of the session closes the card. Roles
50
- * carry the look (gradient logo, type scale, column hairline); a "[NEW]" tip
27
+ * A `card` (`omp.welcome`) mirroring the terminal banner: the lockup
28
+ * (`omp.welcome.lockup`: the terminal's builtin `omp` mark, which it animates,
29
+ * beside the wordmark with the version under it) and the tip of the session.
30
+ * Roles carry the look (gradient logo, type scale); a "[NEW]" tip
51
31
  * carries a terminal-clocked shimmering tag.
52
32
  */
53
33
  describe(_cx: DescribeContext): NativeNode;
54
- /** A click on a recent session resumes it. */
55
- handleNativeEvent(event: NativeUiEvent): void;
56
34
  /** The intro keeps the welcome block mutable; settling lets it retire to history. */
57
35
  isTranscriptBlockFinalized(): boolean;
58
36
  /**
@@ -71,9 +49,6 @@ export declare class WelcomeComponent implements Component {
71
49
  stopIntro(): void;
72
50
  /** Update the version embedded in the welcome border title. */
73
51
  setVersion(version: string): void;
74
- setModel(modelName: string, providerName: string): void;
75
- setRecentSessions(sessions: RecentSession[]): void;
76
- setLspServers(servers: LspServerInfo[] | null): void;
77
52
  render(termWidth: number): readonly string[];
78
53
  }
79
54
  /** Block-grid brand mark shared by the welcome and setup surfaces. */
@@ -3,6 +3,7 @@ import type { CompactionSettings } from "@oh-my-pi/pi-agent-core/compaction";
3
3
  import type { Tool as AiTool, Model } from "@oh-my-pi/pi-ai";
4
4
  import type { Theme } from "../theme/index.js";
5
5
  import { Container } from "../tui.js";
6
+ import type { TspText } from "@oh-my-pi/pi-wire";
6
7
  import type { DescribeContext, NativeNode } from "../native/node.js";
7
8
  interface ContextSkill {
8
9
  readonly name: string;
@@ -154,6 +155,8 @@ export declare function computeNonMessageBreakdown(session: NonMessageTokenSourc
154
155
  * session and model.
155
156
  */
156
157
  export declare function computeContextBreakdown(session: ContextUsageSession, options: ContextUsageOptions): ContextBreakdown;
158
+ /** The `/context` title: the model and its window once a model is selected. */
159
+ export declare function contextUsageHead(breakdown: ContextBreakdown): TspText;
157
160
  /**
158
161
  * Native context-usage panel for terminals without `meter`: the cell grid as
159
162
  * unwrapped styled rows beside the legend. The terminal places the legend
@@ -161,15 +164,14 @@ export declare function computeContextBreakdown(session: ContextUsageSession, op
161
164
  */
162
165
  export declare function describeContextUsage(breakdown: ContextBreakdown): NativeNode;
163
166
  /**
164
- * The `/context` transcript block: ANSI renders the titled cell grid between
165
- * rules; natively it is one frame, drawn with `meter`s where the terminal
166
- * has them and as the glyph grid otherwise.
167
+ * The `/context` report body, without a frame or title of its own (the panel
168
+ * showing it is titled {@link contextUsageHead}): ANSI renders the cell grid
169
+ * beside the legend; natively it is drawn with `meter`s where the terminal has
170
+ * them and as the glyph grid otherwise.
167
171
  */
168
172
  export declare class ContextUsageView extends Container {
169
173
  #private;
170
174
  constructor(breakdown: ContextBreakdown, theme: Theme);
171
- /** Replace the shown breakdown in place, refreshing both the ANSI and native views. */
172
- setBreakdown(breakdown: ContextBreakdown): void;
173
175
  describe(cx: DescribeContext): NativeNode;
174
176
  }
175
177
  /**
@@ -4,7 +4,7 @@ import { ThinkingLevel } from "@oh-my-pi/pi-agent-core/thinking";
4
4
  import { Effort } from "@oh-my-pi/pi-catalog/effort";
5
5
  import type { Model } from "@oh-my-pi/pi-catalog/types";
6
6
  /** Thinking selectors accepted by CLI inputs, in display order. */
7
- export declare const CLI_THINKING_LEVELS: readonly string[];
7
+ export declare const CLI_THINKING_LEVELS: readonly ConfiguredThinkingLevel[];
8
8
  /**
9
9
  * Metadata used to render thinking selector values in the coding-agent UI.
10
10
  */
@@ -108,6 +108,8 @@ export interface TaskItem {
108
108
  solutionSpace?: string;
109
109
  /** Per-spawn thinking effort: lowest/middle/highest level the resolved model supports. Overrides the agent's default selector (e.g. `auto`). */
110
110
  effort?: "lo" | "med" | "hi";
111
+ /** Per-spawn model selector or ordered selector array; overrides agent and settings preferences. */
112
+ model?: string | string[];
111
113
  /** Caller-provided output schema; its presence overrides the selected agent's schema. */
112
114
  outputSchema?: unknown;
113
115
  /** Validation behavior for a caller-provided or inherited output schema. */
@@ -134,6 +136,8 @@ export interface TaskParams {
134
136
  solutionSpace?: string;
135
137
  /** Per-spawn thinking effort (flat form): lowest/middle/highest level the resolved model supports. */
136
138
  effort?: "lo" | "med" | "hi";
139
+ /** Per-spawn model selector or ordered selector array; overrides agent and settings preferences. */
140
+ model?: string | string[];
137
141
  /** Caller-provided output schema; its presence overrides the selected agent's schema. */
138
142
  outputSchema?: unknown;
139
143
  /** Validation behavior for a caller-provided or inherited output schema. */
@@ -255,7 +259,7 @@ export interface AgentProgress {
255
259
  resolvedModelRoute?: string;
256
260
  /** True when a live advisor was attached to this run's session, not merely enabled in settings. */
257
261
  advisor?: boolean;
258
- /** The agent's latest self-estimate of task completion (0–100), from the periodic `task.completionProbeMs` side request. */
262
+ /** The agent's latest self-estimate of task completion (0–100), from the periodic `task.completionProbe` side request. */
259
263
  completionPercent?: number;
260
264
  /** Data extracted by registered subprocess tool handlers (keyed by tool name) */
261
265
  extractedToolData?: Record<string, unknown[]>;
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.12",
4
+ "version": "18.5.0",
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.12",
45
- "@oh-my-pi/pi-agent-core": "18.4.12",
46
- "@oh-my-pi/pi-ai": "18.4.12",
47
- "@oh-my-pi/pi-catalog": "18.4.12",
48
- "@oh-my-pi/pi-natives": "18.4.12",
49
- "@oh-my-pi/pi-utils": "18.4.12",
50
- "@oh-my-pi/pi-wire": "18.4.12",
51
- "@oh-my-pi/snapcompact": "18.4.12"
44
+ "@oh-my-pi/omptype": "18.5.0",
45
+ "@oh-my-pi/pi-agent-core": "18.5.0",
46
+ "@oh-my-pi/pi-ai": "18.5.0",
47
+ "@oh-my-pi/pi-catalog": "18.5.0",
48
+ "@oh-my-pi/pi-natives": "18.5.0",
49
+ "@oh-my-pi/pi-utils": "18.5.0",
50
+ "@oh-my-pi/pi-wire": "18.5.0",
51
+ "@oh-my-pi/snapcompact": "18.5.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "kitty-vt-wasm": "^0.2.0"
@@ -50,7 +50,8 @@ export function hasLinuxDesktopSession(
50
50
  if (platform !== "linux") return false;
51
51
  if (env.DBUS_SESSION_BUS_ADDRESS) return true;
52
52
  const runtimeDir = env.XDG_RUNTIME_DIR;
53
- return Boolean(runtimeDir && fileExists(path.join(runtimeDir, "bus")));
53
+ // Only reached for Linux, so join with POSIX separators regardless of the host.
54
+ return Boolean(runtimeDir && fileExists(path.posix.join(runtimeDir, "bus")));
54
55
  }
55
56
 
56
57
  /**
@@ -29,10 +29,12 @@ export type NativeNode = {
29
29
  */
30
30
  readonly key?: string;
31
31
  /**
32
- * Scroll the node into view, placed like the `reveal` op, when it is
33
- * added. Key a node by what it points at to reveal it again on a move.
32
+ * Scroll the node into view, placed like the `reveal` op. A placement
33
+ * reveals it when it is added: key a node by what it points at to
34
+ * reveal it again on a move. A {@link NativeReveal} reveals it whenever
35
+ * its `n` differs from the previous description of the same node.
34
36
  */
35
- readonly reveal?: "start" | "end" | "nearest";
37
+ readonly reveal?: NativeRevealAt | NativeReveal;
36
38
  /**
37
39
  * Keyboard scrolling forwarded to the terminal (PgUp/PgDn/End reach the
38
40
  * program): moves the scroller at or above the node by `by` whenever
@@ -44,6 +46,19 @@ export type NativeNode = {
44
46
  };
45
47
  }[TspKind];
46
48
 
49
+ /** Where a revealed node lands in its scroller. */
50
+ export type NativeRevealAt = "start" | "end" | "nearest";
51
+
52
+ /**
53
+ * A repeatable {@link NativeNode.reveal}: bump `n` to bring an existing node
54
+ * into view again (a Contents entry jumping to its section). A freshly added
55
+ * node is not revealed.
56
+ */
57
+ export interface NativeReveal {
58
+ readonly at: NativeRevealAt;
59
+ readonly n: number;
60
+ }
61
+
47
62
  /** A {@link NativeNode.scroll} request. */
48
63
  export interface NativeScroll {
49
64
  readonly by: TspScrollBy;
@@ -31,7 +31,7 @@ import * as logger from "@oh-my-pi/pi-utils/logger";
31
31
  import { TSP_TEXT_KINDS, type TspKind, type TspNode, type TspOp, type TspScrollBy } from "@oh-my-pi/pi-wire";
32
32
  import { type Component, Container, CURSOR_MARKER } from "../tui";
33
33
  import { normalizeIconProps } from "./icons";
34
- import type { DescribeContext, NativeChild, NativeNode } from "./node";
34
+ import type { DescribeContext, NativeChild, NativeNode, NativeRevealAt } from "./node";
35
35
  import { isNativeSettled } from "./settle";
36
36
 
37
37
  /** The regions a frame fills. */
@@ -213,8 +213,11 @@ export class Reconciler {
213
213
  #ops: TspOp[] = [];
214
214
  #dels: Entry[] = [];
215
215
  #settles: string[] = [];
216
- /** Selected list items and added `reveal` nodes sent this frame, revealed once the frame's adds have landed. */
217
- #reveals: [id: string, at: "start" | "end" | "nearest"][] = [];
216
+ /**
217
+ * Selected list items, added `reveal` nodes and nodes whose reveal `n` moved this frame,
218
+ * revealed once the frame's adds have landed.
219
+ */
220
+ #reveals: [id: string, at: NativeRevealAt][] = [];
218
221
  /** `scroll` requests whose `n` moved this frame, sent after the reveals. */
219
222
  #scrolls: [id: string, by: TspScrollBy][] = [];
220
223
  /** Wire ids `list.selected` keys resolved to (an unresolved key names no node to reveal). */
@@ -595,6 +598,11 @@ export class Reconciler {
595
598
  this.#revisitNode(next, inner);
596
599
  return;
597
600
  }
601
+ const reveal = next.node.reveal;
602
+ if (typeof reveal === "object") {
603
+ const was = old.node.reveal;
604
+ if (reveal.n !== (typeof was === "object" ? was.n : undefined)) this.#reveals.push([next.id, reveal.at]);
605
+ }
598
606
  if (old.node.k !== next.node.k) {
599
607
  this.#dropSubtree(old);
600
608
  this.#ops.push(["del", next.id]);
@@ -743,7 +751,7 @@ export class Reconciler {
743
751
 
744
752
  /** A full wire subtree for an entry; nested components get states (or pending moves when mounted elsewhere). */
745
753
  #materialize(entry: NodeEntry, walk: Walk): TspNode {
746
- if (entry.node.reveal) this.#reveals.push([entry.id, entry.node.reveal]);
754
+ if (typeof entry.node.reveal === "string") this.#reveals.push([entry.id, entry.node.reveal]);
747
755
  const entries = this.#entries(entry.node.c, entry.keypath, entry.owner, entry.hoist, walk);
748
756
  const children: TspNode[] = [];
749
757
  for (let i = 0; i < entries.length; i++) {
@@ -1,14 +1,13 @@
1
1
  /**
2
- * The async background jobs of a session, twice: {@link JobsPanel} is the
3
- * `/jobs` transcript block (ANSI keeps the caller's text report; natively one
4
- * frame), {@link JobsSheet} the dismissable sheet the native jobs pill opens.
5
- * The panel draws task jobs as `agent` nodes and the others as a status dot,
6
- * the one-line label and a live `elapsed`; the sheet lists every job and
7
- * inspects the selected one (command, cwd, live pids, exit code, output tail).
2
+ * The async background jobs of a session as {@link JobsSheet}: the dismissable
3
+ * sheet the native jobs pill and `/jobs` open. It draws task jobs as `agent`
4
+ * nodes and the others as a status dot, the one-line label and a live
5
+ * `elapsed`, lists every job and inspects the selected one (command, cwd, live
6
+ * pids, exit code, output tail).
8
7
  */
9
8
  import type { TspAgentProps, TspSpan } from "@oh-my-pi/pi-wire";
10
9
  import { formatDuration } from "@oh-my-pi/pi-utils";
11
- import { type Component, Container } from "../tui";
10
+ import type { Component } from "../tui";
12
11
  import { matchesSelectCancel, matchesSelectDown, matchesSelectUp } from "../keybinding-matchers";
13
12
  import { matchesKey } from "../keys";
14
13
  import { truncateToWidth } from "../utils";
@@ -92,31 +91,6 @@ const DOT: Record<JobsPanelJob["status"], TspSpan> = {
92
91
  /** Output tail rows the terminal fallback shows under the selected job. */
93
92
  const FALLBACK_TAIL_LINES = 8;
94
93
 
95
- /** The `/jobs` block: `ansi` renders in the terminal, the snapshot describes the native frame. */
96
- export class JobsPanel extends Container {
97
- readonly #snapshot: JobsPanelSnapshot;
98
- readonly #nowMs: number;
99
- #native: { agent: boolean; node: NativeNode } | undefined;
100
-
101
- constructor(snapshot: JobsPanelSnapshot, nowMs: number, ansi: readonly Component[]) {
102
- super();
103
- this.#snapshot = snapshot;
104
- this.#nowMs = nowMs;
105
- for (const child of ansi) this.addChild(child);
106
- }
107
-
108
- override describe(cx: DescribeContext): NativeNode {
109
- const agent = cx.supports("agent");
110
- if (this.#native?.agent === agent) return this.#native.node;
111
- const head: TspSpan[] = [span("Background jobs", "strong")];
112
- const running = this.#snapshot.running.length;
113
- if (running > 0) head.push(span(` · ${running} running`, "muted"));
114
- const described = card({ role: "omp.jobs", head }, describeJobs(this.#snapshot, this.#nowMs, agent));
115
- this.#native = { agent, node: described };
116
- return described;
117
- }
118
- }
119
-
120
94
  /**
121
95
  * The jobs pill's sheet: a centred `lg` glass sheet titled Background jobs.
122
96
  * A selectable list of every job (running first) sits over the selected
@@ -254,27 +228,6 @@ function jobAge(job: JobsPanelJob, nowMs: number): number {
254
228
  return Math.max(0, (job.endTime ?? nowMs) - job.startTime);
255
229
  }
256
230
 
257
- /** The body the `/jobs` panel draws: running rows, then a Recent section; an empty note when there are none. */
258
- function describeJobs(snapshot: JobsPanelSnapshot, nowMs: number, agent: boolean): NativeChild[] {
259
- const { running, recent } = snapshot;
260
- const children: NativeChild[] = [];
261
- if (running.length === 0 && recent.length === 0) {
262
- children.push(node("text", { text: "No background jobs", role: "omp.jobs.empty" }, undefined, "empty"));
263
- }
264
- for (const job of running) children.push(describeJob(job, nowMs, agent));
265
- if (recent.length > 0) {
266
- children.push(
267
- node(
268
- "section",
269
- { head: [span("Recent", "muted")] },
270
- recent.map(job => describeJob(job, nowMs, agent)),
271
- "recent",
272
- ),
273
- );
274
- }
275
- return children;
276
- }
277
-
278
231
  /** A task job as an `agent` node when the terminal draws them, else a one-line dot row. */
279
232
  function describeJob(job: JobsPanelJob, nowMs: number, agent: boolean): NativeNode {
280
233
  const settled = job.status !== "running";