@aiwayds/dsh-tui-pi 2.5.0 → 2.7.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.
@@ -0,0 +1,268 @@
1
+ /**
2
+ * /history — the read-only two-pane history browser (CONTEXT.md "History
3
+ * browser", ADR 0003, docs/features/history.md).
4
+ *
5
+ * Left pane: a TablePanel listing the browsed session's COMPLETED turns
6
+ * (turn 序号 + user-message preview, seq order — a list, not a tree; the
7
+ * session log has no message-level branching). Right pane: the selected
8
+ * turn's content — the user prompts in the main transcript's bubble style,
9
+ * the assembled LLM replies as Markdown, and a per-tool call-count summary.
10
+ * View and copy only: Enter/`c` refills the editor with the turn's user
11
+ * prompt (a plain setText — never submitted), `s` swaps the browsed session
12
+ * through a /resume-style picker, Esc closes. No resend, no branch, no
13
+ * transcript jump.
14
+ *
15
+ * Snapshot semantics: the event list is read once per open / session switch
16
+ * (live session → `session.snapshotEvents()`, stored session →
17
+ * `sessionPersistence.inspect()` — a cold read, no writer lock, no agent
18
+ * activation). A session that keeps running while the viewer is open does
19
+ * NOT live-update; reopening refreshes.
20
+ *
21
+ * Layout: ≥100 terminal columns renders an HStack (list ≈40% with a 30-column
22
+ * floor, detail takes the remainder); narrower terminals stack the panes
23
+ * vertically (list on top). The container is chosen per render at the current
24
+ * width. The window itself is FIXED geometry: the panel renders exactly
25
+ * `overlayContentBudget()` lines (short content pads blank, long content
26
+ * lives in the detail scroll window), so picking another turn never changes
27
+ * the window size — only a terminal resize re-derives it (overlay mounted at
28
+ * '90%' width / '85%' height; the budget floors the same percentage pi-tui
29
+ * does).
30
+ *
31
+ * Focus model: the keyboard lives on the left list by default; `→` hands it
32
+ * to the detail pane (`↑`/`↓` line-scroll, PgUp/PgDn or `[`/`]` page, `←` or
33
+ * Esc steps back; every other key is inert there). Esc grades detail → list
34
+ * → filter-clear → close — it never skips a level. Focus is visible: the
35
+ * focused pane's cues are the list's ▸ cursor (demoted to `›` while the
36
+ * detail is keyed) versus the detail pane's accent-BOLD title and its
37
+ * `← list · ↑↓ scroll` footer hint.
38
+ *
39
+ * Scroll reality (documented deviation from the original sketch): pi-tui's
40
+ * overlay path composites `component.render(width)` lines directly — the
41
+ * layout engine never descends into overlays, so a ScrollView can never
42
+ * obtain a viewport there (the same limitation AGENTS.md documents for plain
43
+ * Containers). The detail pane therefore manages its own scroll window (the
44
+ * SubagentViewerPanel precedent): `[` / `]` page, selection change resets to
45
+ * the top. The content itself is built from the same pi-tui primitives the
46
+ * main transcript uses (Text bubbles / Markdown), so the look matches.
47
+ *
48
+ * Static rebuild mode (the setTheme/relayout snapshot pattern): switching the
49
+ * selected turn REBUILDS the detail container from the turn's events — the
50
+ * render() path only ever slices an already-built line list and never
51
+ * touches the event data (iron rule 1: render never re-scans). The one
52
+ * deliberate exception is the render-time resize check below: an O(1)
53
+ * budget comparison that may rebuild ALREADY-DERIVED display rows (never
54
+ * events) so the fixed geometry tracks terminal resizes without a resize
55
+ * listener. Unlike the transcript's event-driven relayout(), this runs
56
+ * inside render() by design — do not copy it as a default pattern.
57
+ */
58
+ import type { Context } from '@deepseek-ai/cordis';
59
+ import { type SessionEvent } from '@deepseek-ai/dsh-session';
60
+ import { Container, type Component, type TUI } from '@earendil-works/pi-tui';
61
+ import { type HistoryTurn } from './history-turns.ts';
62
+ import { PanelHost, TablePanel } from './panels.ts';
63
+ import { type TuiTheme } from './theme/index.ts';
64
+ /** Terminal width at which the browser switches from stacked to side-by-side. */
65
+ export declare const DUAL_PANE_MIN_COLUMNS = 100;
66
+ /** Floor of the left list pane in dual-pane mode (spec: min 30 columns). */
67
+ export declare const LEFT_PANE_MIN_COLUMNS = 30;
68
+ /** Rendered-line cap of one user bubble in the detail pane (spec: truncated). */
69
+ export declare const MAX_USER_BUBBLE_LINES = 40;
70
+ /**
71
+ * Content-row budget inside the framed overlay: showOverlay slices the
72
+ * component's lines at maxHeight ('85%' of the terminal — pi-tui floors the
73
+ * percentage, and so do we, so the budget never exceeds the real slice),
74
+ * and the FramedOverlay adds 4 chrome rows (top/bottom border + a blank
75
+ * spacer each). The browser renders EXACTLY this many lines (pad or cap —
76
+ * fixed window geometry), and every inner budget derives from it so no
77
+ * footer is ever sliced off. Testable; `rows` injected.
78
+ */
79
+ export declare function overlayContentBudget(rows?: number | undefined): number;
80
+ /**
81
+ * Visible rows of the left list: the TablePanel chrome is 7 rows (title,
82
+ * ┬/header/┼/┴ rules, blank spacer, footer) and 2 more rows are reserved
83
+ * for the filter line and the status line — the two optional lines that can
84
+ * co-display with results (an applied filter plus a copy/load status), and
85
+ * under-reserving them slices the panel footers off the fixed budget; the
86
+ * stacked layout additionally owes the detail pane its own chrome + a living
87
+ * body (15 reserved rows), the side-by-side layout only the slice guard (9).
88
+ * Fixed at panel construction — a terminal resize mid-open keeps the stale
89
+ * budget until reopened (the accepted overlay behavior). Testable.
90
+ */
91
+ export declare function listMaxVisible(rows?: number | undefined, columns?: number | undefined): number;
92
+ /** One row of the left list: the turn plus its pre-clipped display cells. */
93
+ export interface HistoryRow {
94
+ turn: HistoryTurn;
95
+ /** The log turn number, as displayed. */
96
+ turnLabel: string;
97
+ /** One-line preview (control chars folded, hard character cap). */
98
+ preview: string;
99
+ }
100
+ /**
101
+ * The left-list rows for a turn list under `query`: case-insensitive
102
+ * substring match on the preview text and the turn number, in seq order.
103
+ * Pure; the TablePanel clips `preview` to the cell width at render time.
104
+ */
105
+ export declare function historyRows(turns: readonly HistoryTurn[], query: string): HistoryRow[];
106
+ /**
107
+ * The detail pane's content for one turn, as a fresh Container of the same
108
+ * primitives the main transcript renders: user prompts as canvasSubtle
109
+ * bubbles (Text + bg), replies as Markdown parsed once per rebuild, then the
110
+ * `⚙` tool-count summary line and any turn-end notice. Exported for tests.
111
+ */
112
+ export declare function buildTurnDetailContainer(turn: HistoryTurn, theme: TuiTheme): Container;
113
+ /** Structural TUI/session seam of the browser — keeps the flow testable. */
114
+ export interface HistoryBrowserDeps {
115
+ readonly ctx: Context;
116
+ readonly tui: TUI;
117
+ readonly theme: TuiTheme;
118
+ /** Current live session id, when one exists. */
119
+ getSessionId(): string | undefined;
120
+ /** Event snapshot of the live session (its agent's session.snapshotEvents()). */
121
+ getLiveEvents(): readonly SessionEvent[] | undefined;
122
+ /** Copy target: a plain editor setText (never submitted). */
123
+ copyToEditor(text: string): void;
124
+ /**
125
+ * Show the fork-at-turn confirmation over the open browser; resolves true
126
+ * = fork now. `turnLabel` is the selected turn's number (as displayed),
127
+ * `totalTurns` the session's listed turn count; `cold` is true when the
128
+ * browsed session is not the live one — the fork then detaches the LIVE
129
+ * session, and the dialog must say so instead of hiding it.
130
+ */
131
+ confirmForkAtTurn(turnLabel: string, totalTurns: number, cold: boolean): Promise<boolean>;
132
+ /**
133
+ * Fork-and-switch at the turn boundary: start a new session on the CURRENT
134
+ * preset selection seeded with `seed` (the browsed session's prefix), and
135
+ * switch to it. Resolves when the new session is live; a rejection leaves
136
+ * every existing binding untouched (the browser stays open).
137
+ */
138
+ forkAtTurn(seed: readonly SessionEvent[], parentSessionId: string): Promise<unknown>;
139
+ /** Buffered channel for fork failures (a transcript notice). */
140
+ reportError(message: string): void;
141
+ restoreFocus(): void;
142
+ requestRender(): void;
143
+ }
144
+ /** Events + provenance of one browsed session. */
145
+ interface LoadedSession {
146
+ sessionId: string;
147
+ live: boolean;
148
+ events: readonly SessionEvent[];
149
+ }
150
+ /**
151
+ * The user-facing failure line for a failed session load. Corrupt logs get
152
+ * the ⚠ + repair pointer (the /resume vocabulary — repair itself is an
153
+ * agent-side flow and stays out of this read-only browser).
154
+ */
155
+ export declare function historyLoadErrorMessage(id: string, error: unknown): string;
156
+ /** One row of the `s` session picker (the /resume picker's vocabulary). */
157
+ export interface SessionPickRow {
158
+ id: string;
159
+ updated: string;
160
+ dir: string;
161
+ session: string;
162
+ }
163
+ /**
164
+ * Case-insensitive substring filter over the picker's display vocabulary:
165
+ * the session title (preview/label, the ⚠ and ● markers included), the
166
+ * directory, and the raw session id (paste-an-id narrowing). Empty query
167
+ * matches everything, order preserved. Pure; exported for tests.
168
+ */
169
+ export declare function filterSessionPickRows(rows: readonly SessionPickRow[], query: string): SessionPickRow[];
170
+ /**
171
+ * The browser overlay root: left TablePanel + right detail pane, arranged
172
+ * side-by-side (≥100 columns) or stacked (narrower), the container chosen
173
+ * per render at the current width. The keyboard stays with the left list
174
+ * (navigation, `/` filter, Enter/`c` copy, `s` session switch, Esc close);
175
+ * `[`/`]` page the detail pane; there is no focus management between panes.
176
+ */
177
+ export declare class HistoryBrowserPanel implements Component {
178
+ private readonly deps;
179
+ private readonly host;
180
+ private listOptions;
181
+ private list;
182
+ private readonly detail;
183
+ private listMax;
184
+ private sessionId;
185
+ private live;
186
+ private turns;
187
+ /** The browsed session's raw events — the fork-at-turn slice source. */
188
+ private events;
189
+ private query;
190
+ private rows;
191
+ private status;
192
+ private closed;
193
+ private pickerLoading;
194
+ private forkInProgress;
195
+ /**
196
+ * Where the keyboard lives: the left list (default) or the right detail
197
+ * pane. `→` hands focus to the detail pane, `←`/Esc step back; only the
198
+ * focused pane's keys act (detail focus makes ↑↓ scroll, list keys inert).
199
+ */
200
+ private focus;
201
+ /**
202
+ * The overlay budget the current list panel was built for (and its derived
203
+ * list height): a terminal resize re-derives both — the one explicit
204
+ * external change the fixed-geometry render is allowed to react to.
205
+ */
206
+ private builtBudget;
207
+ /** Set by openHistoryBrowser; delivers the closing echo text. */
208
+ onFinish: ((text: string) => void) | undefined;
209
+ constructor(deps: HistoryBrowserDeps, host: PanelHost, loaded: LoadedSession);
210
+ /**
211
+ * (Re)build the left TablePanel over the current rows — the subagent
212
+ * viewer's swap-the-panel pattern. Columns refit against the live rows
213
+ * (autoColumns scans every row), so a session whose turn numbers gain a
214
+ * digit gets a wider TURN column instead of a clipped one; the cursor
215
+ * lands on `preselect` (row 0 of a freshly loaded session). In-place row
216
+ * swaps (`setQuery`) keep using the retained options object.
217
+ */
218
+ private rebuildListPanel;
219
+ invalidate(): void;
220
+ render(width: number): string[];
221
+ handleInput(data: string): void;
222
+ /** Move the keyboard between the two panes and refresh the focus visuals. */
223
+ private setFocus;
224
+ /** Close the overlay and deliver the closing echo text (once). */
225
+ private finish;
226
+ /** Refill the editor with the turn's user prompt and close (never submit). */
227
+ private copyTurn;
228
+ private copySelected;
229
+ /** Swap the browsed session; failures surface on the browser's status line. */
230
+ private loadAndShow;
231
+ /**
232
+ * Build the `s` session picker over prepared rows. Public so tests can
233
+ * drive the real panel (it mounts as its own overlay through the
234
+ * PanelHost). The filter is the same caller-held-query contract as the
235
+ * main list: `/` engages the input, every keystroke rebuilds the rows
236
+ * (case-insensitive substring over session title, directory and session
237
+ * id — `filterSessionPickRows`) with the cursor following its session
238
+ * across the rebuild, Esc clears the query before popping. The query is
239
+ * picker-local — reset on every `s` (CONTEXT.md "Filter").
240
+ */
241
+ buildSessionPickerPanel(rows: readonly SessionPickRow[]): TablePanel<SessionPickRow>;
242
+ /**
243
+ * Fork at the selected turn (`f`): confirm over the open browser, then
244
+ * hand the turn-bounded seed to the fork-and-switch seam. The browsed
245
+ * session's events are the slice source — live snapshots and cold-read
246
+ * (`inspect`) events fork alike. Cancel or an empty slice (nothing
247
+ * completed to carry) changes nothing; a failed fork keeps the browser
248
+ * open with the failure on the status line plus a buffered notice.
249
+ */
250
+ private forkAtSelectedTurn;
251
+ /** Open the session picker overlay (the browser stays mounted underneath). */
252
+ private openSessionPicker;
253
+ /** Live query swap: rebuild rows, keep the cursor on its turn when visible. */
254
+ private setQuery;
255
+ }
256
+ /** Outcome text of the /history command (the command echo line). */
257
+ export interface HistoryOpenResult {
258
+ text: string;
259
+ error: boolean;
260
+ }
261
+ /**
262
+ * Open the history browser. `sessionIdArg` (from `/history <sessionId>`)
263
+ * cold-reads that session; without one the CURRENT live session is browsed
264
+ * and, when none exists, a hint line is returned instead. Resolves with the
265
+ * closing echo text once the overlay closes (Esc, or copy-to-editor).
266
+ */
267
+ export declare function openHistoryBrowser(deps: HistoryBrowserDeps, sessionIdArg: string | undefined): Promise<HistoryOpenResult>;
268
+ export {};