@oh-my-pi/pi-tui 18.13.2 → 18.13.3

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 (36) hide show
  1. package/CHANGELOG.md +24 -14
  2. package/dist/types/apps/session-picker.d.ts +4 -0
  3. package/dist/types/chat/assistant-message.d.ts +1 -1
  4. package/dist/types/chat/bash-execution.d.ts +2 -0
  5. package/dist/types/chat/display-preferences.d.ts +2 -0
  6. package/dist/types/chat/tool-execution.d.ts +1 -1
  7. package/dist/types/components/editor.d.ts +2 -0
  8. package/dist/types/overlays/annotation-overlay.d.ts +7 -1
  9. package/dist/types/overlays/annotation-types.d.ts +28 -0
  10. package/dist/types/overlays/plan-review-overlay.d.ts +2 -0
  11. package/dist/types/overlays/session-selector.d.ts +13 -2
  12. package/dist/types/theme/theme.d.ts +0 -23
  13. package/dist/types/tools/bash-interactive.d.ts +6 -0
  14. package/dist/types/tui.d.ts +4 -1
  15. package/package.json +10 -10
  16. package/src/apps/session-picker.ts +5 -0
  17. package/src/chat/advisor-message.ts +2 -0
  18. package/src/chat/assistant-message.ts +3 -1
  19. package/src/chat/bash-execution.ts +6 -0
  20. package/src/chat/display-preferences.ts +3 -0
  21. package/src/chat/tool-execution.ts +7 -3
  22. package/src/components/editor.ts +3 -1
  23. package/src/native/backend.ts +2 -64
  24. package/src/overlays/annotation-overlay.ts +125 -37
  25. package/src/overlays/annotation-types.ts +24 -0
  26. package/src/overlays/hook-selector.ts +2 -2
  27. package/src/overlays/plan-review-overlay.ts +13 -2
  28. package/src/overlays/session-selector.ts +195 -26
  29. package/src/overlays/usage-dashboard.ts +5 -12
  30. package/src/prompt/custom-editor.ts +6 -1
  31. package/src/terminal.ts +24 -2
  32. package/src/theme/theme.ts +0 -65
  33. package/src/tools/bash-interactive.ts +6 -1
  34. package/src/tools/job-widget.ts +13 -1
  35. package/src/tools/json-tree.ts +1 -1
  36. package/src/tui.ts +7 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.13.3] - 2026-10-28
6
+
7
+ ### Added
8
+
9
+ - The resume picker renames the selected session in place: F2 opens a dialog prefilled with the current title, Enter commits, Esc cancels ([#17262](https://github.com/can1357/oh-my-pi/pull/17262) by [@Twilight0](https://github.com/Twilight0))
10
+
11
+ ### Changed
12
+
13
+ - Updated Bun engine requirement to ≥1.4.4
14
+
15
+ ### Fixed
16
+
17
+ - Restored WebP and JPEG images stay visible in Kitty scrollback after resuming a session ([#17220](https://github.com/can1357/oh-my-pi/issues/17220)).
18
+ - Escape in an annotation note no longer discards it: the overlay closes and `getDraft()`/`restoreDraft()` let the host keep and reopen the note; **Discard notes and close** (confirmed by a second press) clears them instead, and code review's actions start on Paste so a stray Enter never sends the LLM review ([#17271](https://github.com/can1357/oh-my-pi/pull/17271) by [@H4vC](https://github.com/H4vC), based on [#17230](https://github.com/can1357/oh-my-pi/pull/17230) by [@Shadorain](https://github.com/Shadorain)).
19
+ - Fixed terminal replies such as Tern's `tsp;e;{"ev":"ack",…}` showing up in the shell prompt after quitting omp; the exit drain now waits for the terminal to answer a final DA1 sentinel
20
+ - Fixed the plan review sheet in Tern squeezing the "continue with" model tiers under a long plan, so the model name overlapped the tier buttons
21
+ - `/usage` in Tern shows when each quota resets as a countdown (`resets in 2h30m`) instead of a clock time; hovering shows the date and time
22
+ - Fixed extension `ui.select` dialogs treating unconfigured `j`/`k` keys as navigation, which could turn a default No into Yes when typing `ok` then Enter ([#17255](https://github.com/can1357/oh-my-pi/pull/17255) by [@SAKANA-MOONLIGHT](https://github.com/SAKANA-MOONLIGHT)).
23
+
24
+ ### Removed
25
+
26
+ - Removed support for the TSP `t` (theme) verb, as surface theme synchronization is no longer required
27
+
5
28
  ## [18.13.2] - 2026-10-27
6
29
 
7
30
  ### Added
@@ -2475,17 +2498,4 @@
2475
2498
  - Fixed TUI shutdown placing the parent shell prompt one row below short rendered content instead of directly on the next line ([#1620](https://github.com/can1357/oh-my-pi/issues/1620)).
2476
2499
  - Stopped painting inline color swatches for 4-digit hex runs in Markdown rendering. The `#RGBA` CSS form collides with hashline `#TAG` snapshot tags (4 hex digits, e.g. `#6C5E`), which were sprouting spurious RGB swatches in prose and codespans. Only `#RGB`, `#RRGGBB`, and `#RRGGBBAA` qualify now.
2477
2500
 
2478
- ## [15.7.6] - 2026-06-01
2479
-
2480
- ### Fixed
2481
-
2482
- - Fixed native Windows + Windows Terminal freezing the editor on the wrap keystroke, on `/plan`/`/resume`/model-switch/status-line toggles, and on any other offscreen structural mutation until the next prompt submit. The `15.7.5` `#1635` fix routed every viewport-saturating pure-append and structural mutation through `deferredMutation` (a literal no-op) whenever `isNativeViewportAtBottom()` returned `undefined` — which it always does under `WT_SESSION` because the kernel32 probe can't see WT host scrollback. The deferral was only ever meant for the *confirmed-scrolled* case; an unknown viewport now falls back to a non-destructive `viewportRepaint` instead, so the live UI keeps updating without emitting `\x1b[3J` and without yanking a possibly-scrolled reader. Confirmed-scrolled frames (probe returns `false`) still defer.
2483
- - Removed the hard-coded 20-result cap on `@`-prefixed fuzzy file completion in `CombinedAutocompleteProvider.#getFuzzyFileSuggestions`. The dropdown now honors the existing `maxResults: 100` ceiling already configured for `fuzzyFind`, so projects with many files sharing a common stem (e.g. `@controller`, `@test`) surface all relevant matches instead of being silently truncated. ([#1652](https://github.com/can1357/oh-my-pi/issues/1652))
2484
-
2485
- ## [15.7.5] - 2026-06-01
2486
-
2487
- ### Fixed
2488
-
2489
- - Fixed native Windows + Windows Terminal scrollback being yanked to the top when a streaming response triggered a TUI full redraw. Under ConPTY the `kernel32` `GetConsoleScreenBufferInfo` probe answers about the pseudo-console (always at the buffer tail) and not about WT's host scrollback, so `isNativeViewportAtBottom()` falsely returned `true` while the user was scrolled up and the shrink-across-viewport branch issued a destructive `historyRebuild` (`\x1b[2J\x1b[H\x1b[3J`). The probe now short-circuits to `undefined` whenever `WT_SESSION` is set, letting the existing deferred-rebuild path keep streaming-time mutations non-destructive and reconcile native history at the next prompt-submit checkpoint. ([#1635](https://github.com/can1357/oh-my-pi/issues/1635))
2490
-
2491
- Older entries are archived in [packages/tui/CHANGELOG.md@3d45d8557cd7](https://github.com/can1357/oh-my-pi/blob/3d45d8557cd7493221b69c7ed3c6b6a2307f46e7/packages/tui/CHANGELOG.md).
2501
+ Older entries are archived in [packages/tui/CHANGELOG.md@3deb6751027b](https://github.com/can1357/oh-my-pi/blob/3deb6751027b11b10ac14b2a5c07303817276505/packages/tui/CHANGELOG.md).
@@ -4,6 +4,8 @@ export interface SessionPickerHost<T extends SessionSelectorEntry = SessionSelec
4
4
  loadPinnedIds?(): Promise<ReadonlySet<string>>;
5
5
  loadHistoryMatcher?(): SessionHistoryMatcher;
6
6
  deleteSession?(session: T): Promise<boolean>;
7
+ /** Rename the session and resolve with the title as stored, or null when it did not happen. */
8
+ renameSession?(session: T, title: string): Promise<string | null>;
7
9
  loadAllSessions?(): Promise<T[]>;
8
10
  }
9
11
  /** Presentation and capability controls for the standalone session picker. */
@@ -13,6 +15,8 @@ export interface SessionPickerOptions<T extends SessionSelectorEntry = SessionSe
13
15
  scopeLabel?: string | false;
14
16
  showCwd?: boolean;
15
17
  allowDelete?: boolean;
18
+ /** Rename the selected session; resolve with the title as stored, or null when it did not happen. */
19
+ onRename?: (session: T, title: string) => Promise<string | null>;
16
20
  allowGlobalScope?: boolean;
17
21
  historySearch?: boolean;
18
22
  pinnedIds?: ReadonlySet<string>;
@@ -87,7 +87,7 @@ export declare class AssistantMessageComponent extends Container {
87
87
  */
88
88
  setExpanded(expanded: boolean): void;
89
89
  isTranscriptBlockFinalized(): boolean;
90
- /** Whether a figure still waits for the raster of its final source; the transcript holds retirement meanwhile. */
90
+ /** Hold image and figure rendering until the completed block can enter immutable scrollback. */
91
91
  isTranscriptBlockPending(): boolean;
92
92
  render(width: number): readonly string[];
93
93
  /**
@@ -37,6 +37,8 @@ export declare class BashExecutionComponent extends Container {
37
37
  appendOutput(chunk: string): void;
38
38
  /** Switch to PTY rendering and feed raw terminal bytes through the vterm replay. */
39
39
  appendPtyChunk(chunk: string): void;
40
+ /** Bytes a terminal would send for a key event, honoring the command's cursor-key mode; "" for key releases. */
41
+ encodePtyInput(data: string): string;
40
42
  setComplete(exitCode: number | undefined, cancelled: boolean, options?: {
41
43
  output?: string;
42
44
  truncation?: TruncationMeta;
@@ -6,6 +6,8 @@ export interface ChatTranscriptDisplayPreferences {
6
6
  cacheMissMarker: boolean;
7
7
  showTokenUsage: boolean;
8
8
  showTurnTime: boolean;
9
+ /** Max rows a collapsed advisor card shows below its header; 0 = no cap. */
10
+ advisorCollapsedLines: number;
9
11
  }
10
12
  /** Current transcript display preferences. */
11
13
  export declare const chatTranscriptDisplayPreferences: ChatTranscriptDisplayPreferences;
@@ -102,7 +102,7 @@ export declare class ToolExecutionComponent extends Container {
102
102
  * component-scoped so the TUI reuses every other root subtree (issue #4377).
103
103
  */
104
104
  tickSpinner(frame: number): void;
105
- /** Whether the result's figure still waits for its raster; the transcript holds retirement meanwhile. */
105
+ /** Hold visible results until their figures and Kitty images are ready for immutable scrollback. */
106
106
  isTranscriptBlockPending(): boolean;
107
107
  /**
108
108
  * Whether this block is ready to retire as immutable history. Partial
@@ -98,6 +98,8 @@ export declare class Editor implements Component, Focusable {
98
98
  describeDecorations: ((lines: readonly string[]) => readonly TspEditorDecoration[]) | undefined;
99
99
  /** TSP code language of the buffer (`python`, `bash`): the terminal highlights it in the mono face. */
100
100
  describeLanguage: (() => string | undefined) | undefined;
101
+ /** Label the TSP field to disable host-side editing while another input owner handles raw keys. */
102
+ nativeKeyPassthrough: boolean;
101
103
  /**
102
104
  * TSP chrome around the `editor` node (the prompt composer's chips, mode
103
105
  * chip, send/stop): the root role, the children with `input` placed where
@@ -1,7 +1,7 @@
1
1
  import { type Focusable, type TUI } from "../index.js";
2
2
  import { type KeybindingsManager } from "../app-keybindings.js";
3
3
  import { type Theme } from "../theme/theme.js";
4
- import type { CodeReviewAnnotation, CodeReviewOverlayResult, ReviewDiffFile, TextReviewAnnotation, TextReviewOverlayResult, TextReviewSource } from "./annotation-types.js";
4
+ import type { AnnotationOverlayDraft, CodeReviewAnnotation, CodeReviewOverlayResult, ReviewDiffFile, TextReviewAnnotation, TextReviewOverlayResult, TextReviewSource } from "./annotation-types.js";
5
5
  import type { TspSpan } from "@oh-my-pi/pi-wire";
6
6
  import { type NativeNode, type NativeUiEvent } from "../native/node.js";
7
7
  export declare const CONTINUE_CODE_REVIEW_ACTION = "Continue with LLM review";
@@ -20,6 +20,8 @@ export interface CodeReviewOverlayCallbacks extends AnnotationOverlayCallbacks {
20
20
  export interface TextReviewOverlayCallbacks extends AnnotationOverlayCallbacks {
21
21
  onComplete(result: TextReviewOverlayResult | undefined): void;
22
22
  }
23
+ /** Clears every note and closes; the host then has nothing to keep. Needs a second confirm. */
24
+ export declare const DISCARD_ANNOTATIONS_ACTION = "Discard notes and close";
23
25
  /** Splits a text source into the overlay's 1-based annotatable lines. */
24
26
  export declare function splitTextLines(text: string): string[];
25
27
  /** The native sheet: full-pane glass, titled by the review kind with what is under review after it. */
@@ -38,6 +40,10 @@ export declare class AnnotationOverlay implements Focusable {
38
40
  setUseTerminalCursor(useTerminalCursor: boolean): void;
39
41
  invalidate(): void;
40
42
  dispose(): void;
43
+ /** Notes, the unfinished note, and any source edit, in the form {@link restoreDraft} replays. */
44
+ getDraft(): AnnotationOverlayDraft;
45
+ /** Replay a draft taken from this same source; anything anchored outside it is dropped. */
46
+ restoreDraft(draft: AnnotationOverlayDraft): void;
41
47
  getAnnotations(): CodeReviewAnnotation[];
42
48
  getTextAnnotations(): TextReviewAnnotation[];
43
49
  /** Repo-relative path of the diff file under the cursor, if this is a code review. */
@@ -84,3 +84,31 @@ export interface TextReviewOverlayResult {
84
84
  /** Source text after an external-editor edit; absent when the source was not edited. */
85
85
  editedText?: string;
86
86
  }
87
+ export type AnnotationOverlayDraft = {
88
+ kind: "diff";
89
+ annotations: {
90
+ fileIndex: number;
91
+ sourceIndex: number;
92
+ annotation: CodeReviewAnnotation;
93
+ }[];
94
+ editor?: {
95
+ fileIndex: number;
96
+ sourceIndex: number;
97
+ scope: "file" | "line";
98
+ editingAnnotationIndex?: number;
99
+ text: string;
100
+ };
101
+ } | {
102
+ kind: "text";
103
+ annotations: {
104
+ sourceIndex: number;
105
+ annotation: TextReviewAnnotation;
106
+ }[];
107
+ editedText?: string;
108
+ editor?: {
109
+ sourceIndex: number;
110
+ scope: "text" | "line";
111
+ editingAnnotationIndex?: number;
112
+ text: string;
113
+ };
114
+ };
@@ -74,6 +74,8 @@ export interface PlanReviewOverlayOptions {
74
74
  slider?: HookSelectorSlider;
75
75
  /** Display label for the external-editor key, surfaced in the footer help. */
76
76
  externalEditorLabel?: string;
77
+ /** The external editor's name ("Notepad", "nvim") for the native Edit button; `$EDITOR` when unset. */
78
+ externalEditorName?: string;
77
79
  /** Serializable annotations restored into this overlay instance. */
78
80
  annotationState?: PlanReviewAnnotationState;
79
81
  }
@@ -71,6 +71,7 @@ declare class SessionList<T extends SessionSelectorEntry> implements Component {
71
71
  onExit: () => void;
72
72
  onToggleScope?: () => void;
73
73
  onDeleteRequest?: (session: T) => void;
74
+ onRenameRequest?: (session: T) => void;
74
75
  /** Re-render hook for async list updates (fuzzy scan completion, history merge). */
75
76
  onRequestRender?: () => void;
76
77
  constructor(sessions: T[], showCwd?: boolean, historyMatcher?: SessionHistoryMatcher, getTerminalRows?: () => number, pinnedIds?: ReadonlySet<string>, currentSessionPath?: string | (() => string | undefined));
@@ -79,6 +80,8 @@ declare class SessionList<T extends SessionSelectorEntry> implements Component {
79
80
  /** Cancel pending async search work; idempotent, called on every picker exit path. */
80
81
  dispose(): void;
81
82
  removeSession(sessionPath: string): void;
83
+ /** Update a session's title in place and refresh the visible list, keeping the selection. */
84
+ renameSessionTitle(sessionPath: string, title: string): void;
82
85
  /** Resolve a list-local rendered-line index to a filtered-session index. */
83
86
  hitTestSession(line: number): number | undefined;
84
87
  /** Wheel notch: move the selection one step (clamped, no wrap). */
@@ -93,6 +96,8 @@ declare class SessionList<T extends SessionSelectorEntry> implements Component {
93
96
  resumeSelected(): void;
94
97
  /** Delete / Backspace on an empty query: ask the parent to confirm deleting the selected session. */
95
98
  requestDelete(): void;
99
+ /** F2: ask the parent to rename the selected session. */
100
+ requestRename(): void;
96
101
  /** Empty the search query (the empty state's "Clear search"). */
97
102
  clearSearch(): void;
98
103
  get selectedSession(): T | undefined;
@@ -120,6 +125,12 @@ declare class SessionList<T extends SessionSelectorEntry> implements Component {
120
125
  }
121
126
  export interface SessionSelectorOptions<T extends SessionSelectorEntry = SessionSelectorEntry> {
122
127
  onDelete?: (session: T) => Promise<boolean>;
128
+ /**
129
+ * Rename the selected session to a user-supplied title; resolve with the
130
+ * title as stored (the session canonicalizes it) or null when the rename
131
+ * did not happen.
132
+ */
133
+ onRename?: (session: T, title: string) => Promise<string | null>;
123
134
  historyMatcher?: SessionHistoryMatcher;
124
135
  /** Loads sessions across all projects for the all-projects scope toggle (Tab). */
125
136
  loadAllSessions?: () => Promise<T[]>;
@@ -188,8 +199,8 @@ export declare class SessionSelectorComponent<T extends SessionSelectorEntry = S
188
199
  /**
189
200
  * With the `picker` kind: the data-first session picker (§4.7). Otherwise
190
201
  * scope `tabs`, loading/error message, then the search `input` and session
191
- * `list` — or the delete-confirmation dialog in their place, as on the ANSI
192
- * path — and the key-hint footer.
202
+ * `list` — or the delete-confirmation / rename dialog in their place, as on
203
+ * the ANSI path — and the key-hint footer.
193
204
  */
194
205
  describe(cx: DescribeContext): NativeNode;
195
206
  /** Picker events (root keypath); fallback: scope tab → toggle scope, click on a session → resume it, exactly like a mouse click. */
@@ -114,29 +114,6 @@ export declare function stopThemeWatcher(): void;
114
114
  * Used by HTML export to generate CSS custom properties.
115
115
  */
116
116
  export declare function getResolvedThemeColors(themeName?: string): Promise<Record<string, string>>;
117
- /** One appearance variant of {@link NativeThemePalette}: token name → `#rrggbb`. */
118
- export type NativeThemeVariant = Record<string, string>;
119
- /** omp's resolved theme for a Tern Surface Protocol terminal (the `t` verb body minus `sf`). */
120
- export interface NativeThemePalette {
121
- dark?: NativeThemeVariant;
122
- light?: NativeThemeVariant;
123
- name: {
124
- dark?: string;
125
- light?: string;
126
- };
127
- }
128
- /**
129
- * Cheap identity of everything {@link getNativeThemePalette} depends on;
130
- * recompute the palette only when it changes.
131
- */
132
- export declare function getNativeThemePaletteKey(): string;
133
- /**
134
- * omp's theme for a native surface: with auto theme, the configured dark and
135
- * light themes; otherwise (or while the theme selector previews one) the
136
- * active theme under its own appearance only. Themes without a loadable
137
- * definition (in-memory instances) contribute nothing.
138
- */
139
- export declare function getNativeThemePalette(): NativeThemePalette;
140
117
  /**
141
118
  * Check if a theme is a "light" theme by analyzing its status-line background
142
119
  * luminance. Loads theme JSON synchronously (built-in or custom file on disk)
@@ -6,6 +6,12 @@ import type { Theme } from "../theme/theme.js";
6
6
  export interface BashInteractiveTerminalBackend {
7
7
  resize(columns: number, rows: number): void;
8
8
  }
9
+ /**
10
+ * Translates a TUI key event into the bytes a terminal would send to a PTY
11
+ * child; key releases encode to "". Shared by the agent `pty` console and
12
+ * `!` commands (`BashExecutionComponent.encodePtyInput`).
13
+ */
14
+ export declare function normalizeInputForPty(data: string, applicationCursorKeysMode: boolean): string;
9
15
  /** Interactive terminal overlay driven by an external PTY controller. */
10
16
  export declare class BashInteractiveOverlayComponent implements Component {
11
17
  #private;
@@ -507,7 +507,10 @@ export declare class TUI extends Container {
507
507
  * never come.
508
508
  */
509
509
  addStartListener(listener: StartListener): () => void;
510
- addInputListener(listener: InputListener): () => void;
510
+ /** Registers a key listener; `prepend` gives a foreground input owner first refusal over global shortcuts. */
511
+ addInputListener(listener: InputListener, options?: {
512
+ prepend?: boolean;
513
+ }): () => void;
511
514
  removeInputListener(listener: InputListener): void;
512
515
  stop(): void;
513
516
  /**
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.13.2",
4
+ "version": "18.13.3",
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,20 +41,20 @@
41
41
  "gen:glyphs": "bun scripts/gen-glyphs.ts"
42
42
  },
43
43
  "dependencies": {
44
- "@oh-my-pi/omptype": "18.13.2",
45
- "@oh-my-pi/pi-agent-core": "18.13.2",
46
- "@oh-my-pi/pi-ai": "18.13.2",
47
- "@oh-my-pi/pi-catalog": "18.13.2",
48
- "@oh-my-pi/pi-natives": "18.13.2",
49
- "@oh-my-pi/pi-utils": "18.13.2",
50
- "@oh-my-pi/pi-wire": "18.13.2",
51
- "@oh-my-pi/snapcompact": "18.13.2"
44
+ "@oh-my-pi/omptype": "18.13.3",
45
+ "@oh-my-pi/pi-agent-core": "18.13.3",
46
+ "@oh-my-pi/pi-ai": "18.13.3",
47
+ "@oh-my-pi/pi-catalog": "18.13.3",
48
+ "@oh-my-pi/pi-natives": "18.13.3",
49
+ "@oh-my-pi/pi-utils": "18.13.3",
50
+ "@oh-my-pi/pi-wire": "18.13.3",
51
+ "@oh-my-pi/snapcompact": "18.13.3"
52
52
  },
53
53
  "devDependencies": {
54
54
  "kitty-vt-wasm": "^0.2.0"
55
55
  },
56
56
  "engines": {
57
- "bun": ">=1.3.14"
57
+ "bun": ">=1.4.4"
58
58
  },
59
59
  "files": [
60
60
  "src",
@@ -11,6 +11,8 @@ export interface SessionPickerHost<T extends SessionSelectorEntry = SessionSelec
11
11
  loadPinnedIds?(): Promise<ReadonlySet<string>>;
12
12
  loadHistoryMatcher?(): SessionHistoryMatcher;
13
13
  deleteSession?(session: T): Promise<boolean>;
14
+ /** Rename the session and resolve with the title as stored, or null when it did not happen. */
15
+ renameSession?(session: T, title: string): Promise<string | null>;
14
16
  loadAllSessions?(): Promise<T[]>;
15
17
  }
16
18
 
@@ -21,6 +23,8 @@ export interface SessionPickerOptions<T extends SessionSelectorEntry = SessionSe
21
23
  scopeLabel?: string | false;
22
24
  showCwd?: boolean;
23
25
  allowDelete?: boolean;
26
+ /** Rename the selected session; resolve with the title as stored, or null when it did not happen. */
27
+ onRename?: (session: T, title: string) => Promise<string | null>;
24
28
  allowGlobalScope?: boolean;
25
29
  historySearch?: boolean;
26
30
  pinnedIds?: ReadonlySet<string>;
@@ -63,6 +67,7 @@ export async function selectSession<T extends SessionSelectorEntry>(
63
67
  },
64
68
  {
65
69
  onDelete: options.allowDelete === false ? undefined : host.deleteSession,
70
+ onRename: options.onRename ?? host.renameSession,
66
71
  historyMatcher,
67
72
  loadAllSessions: options.allowGlobalScope === false ? undefined : host.loadAllSessions,
68
73
  allSessions: options.allSessions,
@@ -2,6 +2,7 @@ import { type Component } from "../tui";
2
2
  import { Disclosure } from "../components/disclosure";
3
3
  import { visibleWidth } from "../utils";
4
4
  import type { AdvisorMessageDetails, AdvisorNote, AdvisorSeverity } from "./messages";
5
+ import { chatTranscriptDisplayPreferences } from "./display-preferences";
5
6
  import { formatBadge, replaceTabs, type ToolUIColor, wrapTextWithAnsi } from "../render/render-utils";
6
7
  import { Ellipsis, truncateToWidth } from "../render";
7
8
  import type { Theme } from "../theme";
@@ -274,6 +275,7 @@ export function createAdvisorMessageCard(
274
275
  body: () => new AdvisorNotes(notes, 0, uiTheme),
275
276
  expanded: getExpanded(),
276
277
  paddingX: 1,
278
+ maxCollapsedRows: chatTranscriptDisplayPreferences.advisorCollapsedLines || undefined,
277
279
  });
278
280
  // The tool-output toggle owns expansion state; synchronize the controlled
279
281
  // disclosure from the callback on every render.
@@ -792,8 +792,9 @@ export class AssistantMessageComponent extends Container {
792
792
  return this.#transcriptBlockFinalized;
793
793
  }
794
794
 
795
- /** Whether a figure still waits for the raster of its final source; the transcript holds retirement meanwhile. */
795
+ /** Hold image and figure rendering until the completed block can enter immutable scrollback. */
796
796
  isTranscriptBlockPending(): boolean {
797
+ if (this.#showImages && this.#kittyConversionsAwaited.size > 0) return true;
797
798
  for (const block of this.#figureBlocks.values()) if (block.pending) return true;
798
799
  return false;
799
800
  }
@@ -1615,6 +1616,7 @@ export class AssistantMessageComponent extends Container {
1615
1616
  })
1616
1617
  .catch(() => {
1617
1618
  this.#kittyConversionsAwaited.delete(key);
1619
+ this.#onImageUpdate?.();
1618
1620
  });
1619
1621
  }
1620
1622
  }
@@ -16,6 +16,7 @@ import type { TruncationMeta } from "../tools/output-meta";
16
16
  import { resolveImageOptions } from "../render/render-utils";
17
17
  import { OutputPane } from "../render/output-pane";
18
18
  import { loadXtermTerminal, readTerminalRows, styleTerminalRow } from "../tools/terminal-output";
19
+ import { normalizeInputForPty } from "../tools/bash-interactive";
19
20
  import { getSixelLineMask, isSixelPassthroughEnabled, sanitizeWithOptionalSixelPassthrough } from "../render/sixel";
20
21
  import {
21
22
  buildExecutionFrame,
@@ -286,6 +287,11 @@ export class BashExecutionComponent extends Container {
286
287
  this.#drainPtyQueue();
287
288
  }
288
289
 
290
+ /** Bytes a terminal would send for a key event, honoring the command's cursor-key mode; "" for key releases. */
291
+ encodePtyInput(data: string): string {
292
+ return normalizeInputForPty(data, this.#ptyTerminal?.modes.applicationCursorKeysMode ?? false);
293
+ }
294
+
289
295
  #drainPtyQueue(): void {
290
296
  const terminal = this.#ptyTerminal;
291
297
  if (!terminal || this.#ptyWriting) return;
@@ -6,6 +6,8 @@ export interface ChatTranscriptDisplayPreferences {
6
6
  cacheMissMarker: boolean;
7
7
  showTokenUsage: boolean;
8
8
  showTurnTime: boolean;
9
+ /** Max rows a collapsed advisor card shows below its header; 0 = no cap. */
10
+ advisorCollapsedLines: number;
9
11
  }
10
12
 
11
13
  /** Current transcript display preferences. */
@@ -16,6 +18,7 @@ export const chatTranscriptDisplayPreferences: ChatTranscriptDisplayPreferences
16
18
  cacheMissMarker: false,
17
19
  showTokenUsage: false,
18
20
  showTurnTime: false,
21
+ advisorCollapsedLines: 0,
19
22
  };
20
23
 
21
24
  /** Apply host display preferences without pulling settings into the renderer. */
@@ -711,14 +711,15 @@ export class ToolExecutionComponent extends Container {
711
711
  // Convert async - catch errors from processing
712
712
  convertImageToPngShared(image)
713
713
  .then(converted => {
714
+ this.#kittyConversionsAwaited.delete(key);
714
715
  this.#kittyConverted.set(key, converted);
715
716
  this.#displayInputVersion++;
716
717
  this.#updateDisplay();
717
718
  this.#ui.requestRender();
718
719
  })
719
720
  .catch(() => {
720
- // Ignore conversion failures - display will use original image format
721
721
  this.#kittyConversionsAwaited.delete(key);
722
+ this.#ui.requestRender();
722
723
  });
723
724
  }
724
725
  }
@@ -859,9 +860,12 @@ export class ToolExecutionComponent extends Container {
859
860
  }
860
861
  }
861
862
 
862
- /** Whether the result's figure still waits for its raster; the transcript holds retirement meanwhile. */
863
+ /** Hold visible results until their figures and Kitty images are ready for immutable scrollback. */
863
864
  isTranscriptBlockPending(): boolean {
864
- return this.#toolActivityVisible && this.#figure?.pending === true;
865
+ return (
866
+ this.#toolActivityVisible &&
867
+ ((this.#showImages && this.#kittyConversionsAwaited.size > 0) || this.#figure?.pending === true)
868
+ );
865
869
  }
866
870
 
867
871
  /**
@@ -776,6 +776,8 @@ export class Editor implements Component, Focusable {
776
776
  describeDecorations: ((lines: readonly string[]) => readonly TspEditorDecoration[]) | undefined;
777
777
  /** TSP code language of the buffer (`python`, `bash`): the terminal highlights it in the mono face. */
778
778
  describeLanguage: (() => string | undefined) | undefined;
779
+ /** Label the TSP field to disable host-side editing while another input owner handles raw keys. */
780
+ nativeKeyPassthrough = false;
779
781
  /**
780
782
  * TSP chrome around the `editor` node (the prompt composer's chips, mode
781
783
  * chip, send/stop): the root role, the children with `input` placed where
@@ -1894,7 +1896,7 @@ export class Editor implements Component, Focusable {
1894
1896
  decor: decor.decor,
1895
1897
  ghost: ghost ? plainText(ghost) : undefined,
1896
1898
  placeholder: nativePlaceholder || undefined,
1897
- mode: vim ? VIM_MODE_LABELS[vim.mode] : undefined,
1899
+ mode: this.nativeKeyPassthrough ? "PTY" : vim ? VIM_MODE_LABELS[vim.mode] : undefined,
1898
1900
  lang: this.describeLanguage?.(),
1899
1901
  maxLines: this.#maxHeight,
1900
1902
  };
@@ -39,15 +39,7 @@ import {
39
39
  type TspOp,
40
40
  } from "@oh-my-pi/pi-wire";
41
41
  import type { Terminal } from "../terminal";
42
- import {
43
- bindTheme,
44
- getCurrentThemeName,
45
- getNativeThemePalette,
46
- getNativeThemePaletteKey,
47
- isLightTheme,
48
- type NativeThemePalette,
49
- setNativeSymbolPreset,
50
- } from "../theme/theme";
42
+ import { getCurrentThemeName, isLightTheme, setNativeSymbolPreset } from "../theme/theme";
51
43
  import type { Component, OverlayOptions, RenderScheduler, RenderTimer } from "../tui";
52
44
  import { TspDocument } from "./apply";
53
45
  import { getNativeBlob, type NativeBlob } from "./blobs";
@@ -313,11 +305,6 @@ export class NativeBackend {
313
305
  #sawResize = false;
314
306
  #live = false;
315
307
  #overlayNodes = new Map<Component, { key: string; node: NativeNode }>();
316
- #unbindTheme: (() => void) | undefined;
317
- #palette: NativeThemePalette | undefined;
318
- #paletteKey: string | undefined;
319
- /** Serialized palette last sent, to skip resends of an unchanged theme. */
320
- #paletteSent: string | undefined;
321
308
  /** Pending delivery owns bytes until sent or confirmed held; settled descriptions may already be released. */
322
309
  #blobs = new Map<string, NativeBlob | "sent">();
323
310
  /** `blobs` queries sent, oldest first: the terminal answers each once, in order. */
@@ -366,54 +353,11 @@ export class NativeBackend {
366
353
  if (this.#live) return;
367
354
  this.#live = true;
368
355
  this.#useNerdSymbols(true);
369
- this.#watchTheme();
370
356
  this.#open(this.#inline);
371
357
  setNativeRendering(true);
372
358
  this.render();
373
359
  }
374
360
 
375
- /**
376
- * Any theme swap (switch, selector preview, auto light/dark, file reload)
377
- * schedules a render; `render()` resends the palette when it changed, so
378
- * bursts coalesce into one `t` and no frame is needed for it.
379
- */
380
- #watchTheme(): void {
381
- this.#unbindTheme?.();
382
- let initial = true;
383
- this.#unbindTheme = bindTheme(() => {
384
- if (initial) return;
385
- this.#host.requestRender();
386
- });
387
- initial = false;
388
- }
389
-
390
- /** The current palette, recomputed only when its inputs changed. */
391
- #currentPalette(): NativeThemePalette {
392
- const key = getNativeThemePaletteKey();
393
- if (this.#palette === undefined || key !== this.#paletteKey) {
394
- this.#paletteKey = key;
395
- this.#palette = getNativeThemePalette();
396
- }
397
- return this.#palette;
398
- }
399
-
400
- /** Send the palette to `surface` (right after its `o`, before any `f`). */
401
- #sendPalette(surface: Surface): void {
402
- const palette = this.#currentPalette();
403
- if (!palette.dark && !palette.light) return;
404
- this.#paletteSent = JSON.stringify(palette);
405
- this.#write("t", { sf: surface.id, ...palette });
406
- }
407
-
408
- /** Resend the palette to every open surface when the theme changed since it was sent. */
409
- #refreshPalette(): void {
410
- const key = this.#paletteKey;
411
- const palette = this.#currentPalette();
412
- if (key === this.#paletteKey || JSON.stringify(palette) === this.#paletteSent) return;
413
- this.#sendPalette(this.#inline);
414
- if (this.#screen) this.#sendPalette(this.#screen);
415
- }
416
-
417
361
  /**
418
362
  * Tern's font carries every Nerd Font glyph, and it never answers the Glyph
419
363
  * Protocol, so icons come from the nerd preset (process-local, not the
@@ -435,8 +379,6 @@ export class NativeBackend {
435
379
  this.#close(this.#inline, keep);
436
380
  this.#inline.doc?.close();
437
381
  setNativeRendering(false);
438
- this.#unbindTheme?.();
439
- this.#unbindTheme = undefined;
440
382
  this.#useNerdSymbols(false);
441
383
  this.#clearStallTimer();
442
384
  // Replies no longer reach the backend: forget the queries outright.
@@ -455,7 +397,6 @@ export class NativeBackend {
455
397
  this.#applyHello(hello);
456
398
  this.#live = true;
457
399
  this.#useNerdSymbols(true);
458
- this.#watchTheme();
459
400
  this.#resetBlobs();
460
401
  const surface = this.#inline;
461
402
  surface.reconciler.detachLive();
@@ -466,7 +407,6 @@ export class NativeBackend {
466
407
  surface.focus = null;
467
408
  surface.dirty = false;
468
409
  this.#write("o", { id: surface.id, mode: "inline", title: "omp", role: SESSION_ROLE, adopt: true });
469
- this.#sendPalette(surface);
470
410
  // After the `o`, as in `start()`.
471
411
  setNativeRendering(true);
472
412
  this.render();
@@ -508,7 +448,6 @@ export class NativeBackend {
508
448
  /** Reconcile and send a frame, unless the target surface is out of credits. */
509
449
  render(): void {
510
450
  if (!this.#live) return;
511
- this.#refreshPalette();
512
451
  const overlays = this.#host.overlays();
513
452
  let fullscreen = -1;
514
453
  for (let i = overlays.length - 1; i >= 0; i--) {
@@ -608,7 +547,6 @@ export class NativeBackend {
608
547
 
609
548
  #open(surface: Surface): void {
610
549
  this.#write("o", { id: surface.id, mode: surface.mode, title: "omp", role: surface.role });
611
- this.#sendPalette(surface);
612
550
  }
613
551
 
614
552
  #close(surface: Surface, keep: boolean): void {
@@ -776,7 +714,7 @@ export class NativeBackend {
776
714
  }
777
715
  }
778
716
 
779
- #write(verb: "o" | "f" | "t" | "x" | "q", body: unknown): void {
717
+ #write(verb: "o" | "f" | "x" | "q", body: unknown): void {
780
718
  this.#record("out", verb, undefined, body);
781
719
  this.#host.terminal.write(encodeTspJson(verb, body, undefined, this.#limit));
782
720
  }