@linxiraos/pi-tui 1.3.1 → 1.4.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,293 @@
1
+ /**
2
+ * Native (`picker`) mapping of the resume directory model.
3
+ *
4
+ * The coding-agent owns the data model (`ResumeDirectory` / `buildResumeView`:
5
+ * project → workspace → conversation). pi-tui cannot import it (dependency
6
+ * direction), so this module declares structurally compatible `*Like` types
7
+ * and accepts a {@link ResumeSourceLike} the host builds from its real
8
+ * directory and view builder (the ANSI tree passes the same object). Nothing
9
+ * here re-groups conversations: the rows come from the model, and this module
10
+ * only turns them into picker props.
11
+ */
12
+ import type { TspPickerGroup, TspPickerItem, TspPickerScope } from "@linxiraos/pi-wire";
13
+ import type { SessionSelectorStatus } from "./session-selector.js";
14
+ import type { NativeChild } from "../native/node.js";
15
+ export type ResumeScopeKindLike = "workspace" | "project" | "all";
16
+ export interface ResumeScopeLike {
17
+ readonly kind: ResumeScopeKindLike;
18
+ readonly key?: string;
19
+ readonly projectKey?: string;
20
+ readonly workspaceKey?: string;
21
+ }
22
+ export type ResumeScopeInputLike = ResumeScopeKindLike | ResumeScopeLike;
23
+ /** Parsed query, as far as the native renderer reads it (the ANSI tree passes a subset). */
24
+ export interface ResumeQueryLike {
25
+ readonly raw: string;
26
+ readonly terms?: readonly string[];
27
+ readonly project?: readonly string[];
28
+ readonly workspace?: readonly string[];
29
+ readonly branch?: readonly string[];
30
+ readonly status?: readonly string[];
31
+ readonly current?: boolean | null;
32
+ readonly fork?: boolean | null;
33
+ readonly pinned?: boolean | null;
34
+ }
35
+ /** The session fields the picker reads; the host's `SessionInfo` supplies all of them. */
36
+ export interface ResumeSessionLike {
37
+ readonly path: string;
38
+ readonly id: string;
39
+ readonly cwd: string;
40
+ readonly title?: string;
41
+ readonly modified: Date;
42
+ readonly size?: number;
43
+ readonly firstMessage?: string;
44
+ readonly allMessagesText?: string;
45
+ readonly status?: SessionSelectorStatus;
46
+ readonly parentSessionPath?: string;
47
+ readonly created?: Date;
48
+ readonly messageCount?: number;
49
+ readonly assistantTurns?: number;
50
+ /** Last model recorded in the session (`provider/modelId`), when one was persisted. */
51
+ readonly model?: string;
52
+ }
53
+ export interface ResumeConversationLike {
54
+ readonly session: ResumeSessionLike;
55
+ readonly key: string;
56
+ readonly id: string;
57
+ readonly path: string;
58
+ readonly title: string;
59
+ readonly cwd: string;
60
+ readonly projectKey: string;
61
+ readonly workspaceKey: string;
62
+ readonly created: Date;
63
+ readonly modified: Date;
64
+ readonly status?: SessionSelectorStatus;
65
+ readonly pinned: boolean;
66
+ readonly current: boolean;
67
+ readonly fork: boolean;
68
+ /** Subagent marker from `session_init.agent`; forks never set it. */
69
+ readonly isSubagent?: boolean;
70
+ readonly agentName?: string;
71
+ readonly parentKey?: string;
72
+ readonly subagents?: readonly ResumeConversationLike[];
73
+ readonly messageCount: number;
74
+ }
75
+ export interface ResumeWorkspaceLike {
76
+ readonly key: string;
77
+ readonly path: string;
78
+ readonly name: string;
79
+ readonly alias?: string | null;
80
+ readonly displayName?: string;
81
+ readonly projectKey: string;
82
+ readonly branch: string | null;
83
+ readonly isWorktree?: boolean;
84
+ readonly pinned: boolean;
85
+ readonly lastActivity: Date | null;
86
+ /** Flat conversation list; the core model keeps conversations on the directory only. */
87
+ readonly conversations?: readonly ResumeConversationLike[];
88
+ readonly counts: {
89
+ readonly conversations: number;
90
+ readonly pinnedConversations: number;
91
+ };
92
+ }
93
+ export interface ResumeProjectLike {
94
+ readonly key: string;
95
+ readonly root: string;
96
+ readonly name: string;
97
+ readonly alias: string | null;
98
+ readonly displayName: string;
99
+ readonly isDefault: boolean;
100
+ readonly pinned: boolean;
101
+ readonly removable?: boolean;
102
+ readonly collapsed: boolean;
103
+ readonly lastActivity: Date | null;
104
+ readonly workspaces?: readonly ResumeWorkspaceLike[];
105
+ readonly conversations?: readonly ResumeConversationLike[];
106
+ readonly counts: {
107
+ readonly conversations: number;
108
+ readonly workspaces: number;
109
+ readonly pinnedConversations: number;
110
+ };
111
+ }
112
+ export interface ResumeDirectoryLike {
113
+ readonly projects: readonly ResumeProjectLike[];
114
+ readonly workspaces: readonly ResumeWorkspaceLike[];
115
+ readonly conversations: readonly ResumeConversationLike[];
116
+ readonly defaultProjectKey: string;
117
+ readonly defaultWorkspaceKey: string;
118
+ readonly counts: {
119
+ readonly projects: number;
120
+ readonly workspaces: number;
121
+ readonly conversations: number;
122
+ readonly pinnedConversations: number;
123
+ };
124
+ }
125
+ export interface ResumeProjectRowLike {
126
+ readonly type: "project";
127
+ readonly key: string;
128
+ readonly project: ResumeProjectLike;
129
+ readonly depth?: 0;
130
+ readonly pinned: boolean;
131
+ readonly collapsed: boolean;
132
+ readonly conversationCount: number;
133
+ readonly totalConversationCount: number;
134
+ readonly workspaceCount: number;
135
+ readonly totalWorkspaceCount: number;
136
+ }
137
+ export interface ResumeWorkspaceRowLike {
138
+ readonly type: "workspace";
139
+ readonly key: string;
140
+ readonly workspace: ResumeWorkspaceLike;
141
+ readonly projectKey: string;
142
+ readonly depth?: 1;
143
+ readonly pinned: boolean;
144
+ readonly conversationCount: number;
145
+ readonly totalConversationCount: number;
146
+ }
147
+ export interface ResumeConversationRowLike {
148
+ readonly type: "conversation";
149
+ readonly key: string;
150
+ readonly conversation: ResumeConversationLike;
151
+ readonly projectKey: string;
152
+ readonly workspaceKey: string;
153
+ readonly depth?: number;
154
+ readonly pinned: boolean;
155
+ readonly current: boolean;
156
+ readonly score?: number;
157
+ readonly pinnedOnly?: boolean;
158
+ }
159
+ export type ResumeViewRowLike = ResumeProjectRowLike | ResumeWorkspaceRowLike | ResumeConversationRowLike;
160
+ export interface ResumeViewCountsLike {
161
+ readonly conversations: number;
162
+ readonly workspaces?: number;
163
+ readonly projects?: number;
164
+ readonly pinnedConversations?: number;
165
+ readonly totalConversations: number;
166
+ readonly totalWorkspaces?: number;
167
+ readonly totalProjects?: number;
168
+ }
169
+ export interface ResumeViewLike {
170
+ readonly scope: ResumeScopeLike;
171
+ readonly query: ResumeQueryLike;
172
+ readonly rows: readonly ResumeViewRowLike[];
173
+ /** Conversation rows; the ANSI tree leaves it out and read `rows` instead. */
174
+ readonly conversations?: readonly ResumeConversationRowLike[];
175
+ readonly counts: ResumeViewCountsLike;
176
+ readonly referenceBranch?: string | null;
177
+ readonly currentWorkspaceKey?: string;
178
+ }
179
+ export interface ResumeViewOptionsLike {
180
+ scope?: ResumeScopeInputLike;
181
+ query?: string;
182
+ currentSessionPath?: string;
183
+ }
184
+ /** Names and per-scope conversation totals for the segmented control. */
185
+ export interface ResumeScopeSummaryLike {
186
+ readonly workspaceName: string;
187
+ readonly projectName: string;
188
+ readonly counts: Readonly<Record<ResumeScopeKindLike, number>>;
189
+ }
190
+ /**
191
+ * The host's resume model: the loaded directory plus its pure view builder.
192
+ * Shared with the ANSI tree, which supplies the same object; `directory` is
193
+ * null until the first scan finishes, and `buildView` returns null then.
194
+ */
195
+ export interface ResumeSourceLike {
196
+ readonly directory: ResumeDirectoryLike | null;
197
+ readonly initialScope?: ResumeScopeKindLike;
198
+ readonly deleteMode?: "trash" | "permanent";
199
+ buildView(options: ResumeViewOptionsLike): ResumeViewLike | null;
200
+ scopeSummary?(): ResumeScopeSummaryLike;
201
+ persistScope?(scope: ResumeScopeKindLike): void | Promise<void>;
202
+ refresh?(): Promise<void>;
203
+ pinConversation?(conversation: ResumeConversationLike, pinned: boolean): void | Promise<void>;
204
+ deleteConversation?(conversation: ResumeConversationLike): boolean | Promise<boolean>;
205
+ isLoading?(): boolean;
206
+ loadError?(): string | undefined;
207
+ }
208
+ export declare const RESUME_SCOPE_ORDER: readonly ResumeScopeKindLike[];
209
+ /** The scope after `kind` in the three-way cycle (Tab). */
210
+ export declare function nextResumeScope(kind: ResumeScopeKindLike): ResumeScopeKindLike;
211
+ /** The segmented control's label for a scope kind. */
212
+ export declare function resumeScopeLabel(kind: ResumeScopeKindLike): string;
213
+ /**
214
+ * Per-scope conversation totals for the segmented control. The host summary
215
+ * wins when present (the ANSI tree already computes it); otherwise the active
216
+ * scope's own pre-query total is reused and the other scopes cost one
217
+ * empty-query view each, so the model's scope resolution (current session →
218
+ * default workspace) decides the group, not this module.
219
+ */
220
+ export declare function resumeScopeTabs(source: ResumeSourceLike, active: ResumeViewLike, currentSessionPath: string | undefined): TspPickerScope[];
221
+ /** Assistant turns the session persisted; older listings fall back to the message count. */
222
+ export declare function resumeTurnCount(conversation: ResumeConversationLike): number;
223
+ export interface ResumeItemOptions {
224
+ /** Show each conversation's working directory (multi-worktree scopes). */
225
+ readonly showCwd?: boolean;
226
+ readonly now?: number;
227
+ /** Branch of the current workspace; a repeated branch stays off the cards. */
228
+ readonly referenceBranch?: string | null;
229
+ }
230
+ /** One conversation card: the existing facts plus turns, branch and model. */
231
+ export declare function resumePickerItem(row: ResumeConversationRowLike, workspace: ResumeWorkspaceLike | undefined, options?: ResumeItemOptions): TspPickerItem;
232
+ /** Project and workspace section heads followed by their conversations, from the model's rows. */
233
+ export declare function resumePickerOrder(view: ResumeViewLike): (string | TspPickerGroup)[];
234
+ /** Free-text hit ranges for the displayed rows (structured tokens never highlight). */
235
+ export declare function resumePickerHits(view: ResumeViewLike, items: ReadonlyMap<string, TspPickerItem>): Record<string, readonly (readonly [number, number])[]> | undefined;
236
+ /** Catalogue items for every conversation in the target scope (identity stable while typing). */
237
+ export declare function resumePickerItems(catalogue: ResumeViewLike, directory: ResumeDirectoryLike, options?: ResumeItemOptions): {
238
+ items: TspPickerItem[];
239
+ byPath: Map<string, TspPickerItem>;
240
+ };
241
+ /** The selected conversation path: the caller's when still visible, else the first row. */
242
+ export declare function resumePickerSelection(view: ResumeViewLike, preferred: string | null | undefined): string | null;
243
+ /**
244
+ * The same view with locally deleted conversations (and the section heads
245
+ * left empty by them) removed. Deletes are host-driven; the directory is not
246
+ * rebuilt per keystroke, so the picker hides the removed rows itself.
247
+ */
248
+ export declare function resumeViewWithout(view: ResumeViewLike, hidden: ReadonlySet<string>): ResumeViewLike;
249
+ export interface ResumePickerModel {
250
+ readonly view: ResumeViewLike;
251
+ readonly catalogue: ResumeViewLike;
252
+ readonly items: readonly TspPickerItem[];
253
+ readonly order: readonly (string | TspPickerGroup)[];
254
+ readonly hits?: Readonly<Record<string, readonly (readonly [number, number])[]>>;
255
+ readonly tabs: readonly TspPickerScope[];
256
+ readonly selected: string | null;
257
+ readonly current: readonly string[];
258
+ readonly total: number;
259
+ /** Conversations the filter hides (the ANSI tree stays on screen while filtering). */
260
+ readonly hidden: number;
261
+ readonly empty: string;
262
+ readonly placeholder: string;
263
+ }
264
+ export interface ResumePickerModelOptions {
265
+ readonly scope: ResumeScopeInputLike;
266
+ readonly query: string;
267
+ readonly currentSessionPath?: string;
268
+ readonly hidden?: ReadonlySet<string>;
269
+ readonly showCwd?: boolean;
270
+ readonly now?: number;
271
+ /** Selection to keep when it survives the query; the first row otherwise. */
272
+ readonly preferredPath?: string | null;
273
+ }
274
+ /**
275
+ * Everything the native sheet needs for one scope + query: the model's
276
+ * filtered view (rows, counts, tokens), a stable catalogue built without the
277
+ * query, the picker items for it, section order, hits, scope tabs and copy.
278
+ */
279
+ export declare function buildResumePickerModel(source: ResumeSourceLike, options: ResumePickerModelOptions): ResumePickerModel | undefined;
280
+ /** True when the view carries any filter (free text or structured token). */
281
+ export declare function resumeQueryActive(view: ResumeViewLike): boolean;
282
+ /** The empty pane's copy: no matches while filtering, else the scope's own empty wording. */
283
+ export declare function resumeEmptyText(view: ResumeViewLike): string;
284
+ /** The search field's placeholder for the active scope (names come from the host summary). */
285
+ export declare function resumePlaceholder(source: ResumeSourceLike, scope: ResumeScopeInputLike): string;
286
+ /** The selected conversation's preview: project, workspace, branch, status, model, then the excerpt. */
287
+ export declare function resumeConversationPreview(conversation: ResumeConversationLike, project: ResumeProjectLike | undefined, workspace: ResumeWorkspaceLike | undefined): NativeChild[];
288
+ /** Project/workspace lookups for a conversation path, derived from the directory (no re-grouping). */
289
+ export declare function resumeConversationContext(directory: ResumeDirectoryLike, path: string): {
290
+ conversation: ResumeConversationLike;
291
+ project: ResumeProjectLike | undefined;
292
+ workspace: ResumeWorkspaceLike | undefined;
293
+ } | undefined;
@@ -0,0 +1,353 @@
1
+ /**
2
+ * ANSI three-level resume tree: project → workspace → conversation.
3
+ *
4
+ * Renders the rows produced by the coding-agent resume view model; it never
5
+ * groups sessions itself. The model arrives through {@link ResumeTreeSource},
6
+ * which the host builds from `loadResumeDirectory` + `buildResumeView` (pi-tui
7
+ * cannot depend on the coding-agent package). Scope cycling, collapse state,
8
+ * search focus, pin actions and the delete confirmation live here; view
9
+ * construction and persistence live in the host.
10
+ */
11
+ import * as path from "node:path";
12
+ import { type Component } from "../index.js";
13
+ export type ResumeTreeScope = "workspace" | "project" | "all";
14
+ /** Persisted picker state (v2 file): scope plus local expansion. */
15
+ export interface ResumeTreePickerState {
16
+ scope: ResumeTreeScope;
17
+ collapsedProjects?: readonly string[];
18
+ expandedProjects?: readonly string[];
19
+ collapsedWorkspaces?: readonly string[];
20
+ /** Absolute paths in the project/workspace/cwd labels; absent means false. */
21
+ fullPaths?: boolean;
22
+ }
23
+ export type ResumeTreeStatus = "complete" | "interrupted" | "aborted" | "error" | "pending" | "unknown";
24
+ export type ResumeTreeDeleteMode = "trash" | "permanent";
25
+ /** The session fields the tree reads from a conversation row. */
26
+ export interface ResumeTreeSession {
27
+ path: string;
28
+ id: string;
29
+ cwd: string;
30
+ size?: number;
31
+ title?: string;
32
+ modified: Date;
33
+ firstMessage?: string;
34
+ }
35
+ export interface ResumeTreeConversation {
36
+ session: ResumeTreeSession;
37
+ key: string;
38
+ id: string;
39
+ path: string;
40
+ title: string;
41
+ cwd: string;
42
+ projectKey: string;
43
+ workspaceKey: string;
44
+ created: Date;
45
+ modified: Date;
46
+ status?: ResumeTreeStatus;
47
+ pinned: boolean;
48
+ current: boolean;
49
+ fork: boolean;
50
+ /** Subagent marker from `session_init.agent`; forks never set it. */
51
+ isSubagent?: boolean;
52
+ agentName?: string;
53
+ parentKey?: string;
54
+ /** Direct subagent children, when the directory attached them. */
55
+ subagents?: readonly ResumeTreeConversation[];
56
+ messageCount: number;
57
+ }
58
+ export interface ResumeTreeWorkspace {
59
+ key: string;
60
+ path: string;
61
+ name: string;
62
+ /** User-configured alias, or null. The directory name never changes. */
63
+ alias?: string | null;
64
+ /** Alias when set, otherwise `name`. */
65
+ displayName?: string;
66
+ projectKey: string;
67
+ branch: string | null;
68
+ pinned: boolean;
69
+ lastActivity: Date | null;
70
+ counts: {
71
+ conversations: number;
72
+ pinnedConversations: number;
73
+ };
74
+ }
75
+ export interface ResumeTreeProject {
76
+ key: string;
77
+ root: string;
78
+ name: string;
79
+ alias: string | null;
80
+ displayName: string;
81
+ isDefault: boolean;
82
+ pinned: boolean;
83
+ collapsed: boolean;
84
+ lastActivity: Date | null;
85
+ counts: {
86
+ conversations: number;
87
+ workspaces: number;
88
+ pinnedConversations: number;
89
+ };
90
+ }
91
+ export interface ResumeTreeProjectRow {
92
+ type: "project";
93
+ key: string;
94
+ project: ResumeTreeProject;
95
+ pinned: boolean;
96
+ collapsed: boolean;
97
+ conversationCount: number;
98
+ totalConversationCount: number;
99
+ workspaceCount: number;
100
+ totalWorkspaceCount: number;
101
+ }
102
+ export interface ResumeTreeWorkspaceRow {
103
+ type: "workspace";
104
+ key: string;
105
+ workspace: ResumeTreeWorkspace;
106
+ projectKey: string;
107
+ pinned: boolean;
108
+ conversationCount: number;
109
+ totalConversationCount: number;
110
+ }
111
+ export interface ResumeTreeConversationRow {
112
+ type: "conversation";
113
+ key: string;
114
+ conversation: ResumeTreeConversation;
115
+ projectKey: string;
116
+ workspaceKey: string;
117
+ pinned: boolean;
118
+ current: boolean;
119
+ pinnedOnly?: boolean;
120
+ }
121
+ export type ResumeTreeRow = ResumeTreeProjectRow | ResumeTreeWorkspaceRow | ResumeTreeConversationRow;
122
+ export interface ResumeTreeView {
123
+ scope: {
124
+ kind: ResumeTreeScope;
125
+ projectKey?: string;
126
+ workspaceKey?: string;
127
+ };
128
+ query: {
129
+ raw: string;
130
+ terms?: readonly string[];
131
+ };
132
+ rows: readonly ResumeTreeRow[];
133
+ conversations?: readonly ResumeTreeConversationRow[];
134
+ counts: {
135
+ conversations: number;
136
+ workspaces?: number;
137
+ projects?: number;
138
+ totalConversations: number;
139
+ totalWorkspaces?: number;
140
+ totalProjects?: number;
141
+ };
142
+ /** Branch of the current session's workspace, when known. */
143
+ referenceBranch?: string | null;
144
+ /** Workspace key of the current session, when known. */
145
+ currentWorkspaceKey?: string;
146
+ }
147
+ /** Names and per-scope conversation totals for the scope header. */
148
+ export interface ResumeTreeScopeSummary {
149
+ workspaceName: string;
150
+ projectName: string;
151
+ counts: Record<ResumeTreeScope, number>;
152
+ }
153
+ /**
154
+ * Host bridge. `buildView` must be a pure projection of the loaded directory;
155
+ * `refresh`/`pin*`/`deleteConversation` perform I/O and resolve when the
156
+ * source's internal state is current again.
157
+ */
158
+ export interface ResumeTreeSource {
159
+ readonly initialScope: ResumeTreeScope;
160
+ buildView(input: {
161
+ scope: ResumeTreeScope;
162
+ query: string;
163
+ currentSessionPath?: string;
164
+ }): ResumeTreeView | null;
165
+ scopeSummary(): ResumeTreeScopeSummary;
166
+ currentSessionPath?(): string | undefined;
167
+ /** Remember the chosen scope across opens. */
168
+ persistScope?(scope: ResumeTreeScope): void | Promise<void>;
169
+ /** Restored v2 picker state (scope + expansion); absent means defaults. */
170
+ pickerState?(): ResumeTreePickerState;
171
+ /** Persist scope + expansion together (preferred over `persistScope`). */
172
+ persistPickerState?(state: ResumeTreePickerState): void | Promise<void>;
173
+ /** Re-read the directory; resolves once `buildView` reflects the new state. */
174
+ refresh?(): Promise<void>;
175
+ /** Partial scan failures (unreadable project folders) while rows are still listed. */
176
+ scanWarningNames?(): readonly string[];
177
+ /** A failed load: no rows available at all. */
178
+ loadError?(): string | undefined;
179
+ isLoading?(): boolean;
180
+ pinConversation?(conversation: ResumeTreeConversation, pinned: boolean): Promise<void>;
181
+ pinProject?(project: ResumeTreeProject, pinned: boolean): Promise<void>;
182
+ pinWorkspace?(workspace: ResumeTreeWorkspace, pinned: boolean): Promise<void>;
183
+ /** Set the workspace display alias; resolves the name the picker now shows. */
184
+ renameWorkspace?(workspace: ResumeTreeWorkspace, alias: string | null): Promise<string | null>;
185
+ openWorkspace?(workspace: ResumeTreeWorkspace): void | Promise<void>;
186
+ /** Rename the persisted session title; resolves the stored name or null. */
187
+ renameConversation?(conversation: ResumeTreeConversation, title: string): Promise<string | null>;
188
+ /** False hides the conversation rename action (read-only/foreign session). */
189
+ canRenameConversation?(conversation: ResumeTreeConversation): boolean;
190
+ /** Scopes without a project header (workspace/project) resolve the project here. */
191
+ projectByKey?(key: string): ResumeTreeProject | undefined;
192
+ deleteConversation?(conversation: ResumeTreeConversation): Promise<boolean>;
193
+ deleteMode: ResumeTreeDeleteMode;
194
+ }
195
+ export interface ResumeTreeOptions {
196
+ source: ResumeTreeSource;
197
+ getTerminalRows?: () => number;
198
+ onSelect: (conversation: ResumeTreeConversation) => void;
199
+ onCancel: () => void;
200
+ onExit: () => void;
201
+ onRequestRender?: () => void;
202
+ /** Start with the search field focused (the default for a resume picker). */
203
+ searchFocused?: boolean;
204
+ }
205
+ /** Short relative age, localised through the shared session-picker keys. */
206
+ export declare function formatResumeAge(date: Date, now?: number): string;
207
+ /** Middle-ellipsis truncation for path-like text (`a/b/…/x/y`). */
208
+ export declare function middleEllipsis(text: string, maxWidth: number): string;
209
+ /** Pick the path API from the string's own shape, not the host platform. */
210
+ export declare function resumePathApi(value: string): typeof path.posix;
211
+ /** Full path spelled with its own platform's separators (drive, UNC or POSIX). */
212
+ export declare function absoluteResumePath(value: string): string;
213
+ /** Short parent/name tail or the full absolute path, per the picker toggle. */
214
+ export declare function formatResumePath(value: string, fullPaths: boolean): string;
215
+ /** Basename-first path tail (`parent/name`) keeping the platform separator. */
216
+ export declare function resumePathTail(value: string): string;
217
+ /**
218
+ * Truncate from the left so the tail stays readable, keeping a drive/UNC root
219
+ * when there is room for one (`D:\…\repo`, `\\server\share\…\repo`). The
220
+ * returned string never exceeds `maxWidth` visible columns.
221
+ */
222
+ export declare function truncateResumePath(value: string, maxWidth: number): string;
223
+ /**
224
+ * A workspace branch is noise when it repeats the current workspace's branch;
225
+ * it is kept for the current workspace or when it actually differs.
226
+ */
227
+ export declare function shouldShowBranch(branch: string | null | undefined, referenceBranch: string | null | undefined, current: boolean, workspaceName?: string): boolean;
228
+ /** Stable right-hand column widths for the conversation list. */
229
+ export interface ConversationColumns {
230
+ status: number;
231
+ turns: number;
232
+ age: number;
233
+ flags: number;
234
+ }
235
+ /** Width of the status column, fixed across rows so every title starts together. */
236
+ export declare function statusColumnWidth(): number;
237
+ /** Column widths across every visible conversation row, so columns stay put while scrolling. */
238
+ export declare function conversationColumns(rows: readonly ResumeTreeConversationRow[], now?: number): ConversationColumns;
239
+ /**
240
+ * One dense conversation row: `status | title | turns | age | flags | ⋯`.
241
+ * The status cell and every metadata column have fixed widths computed once
242
+ * per frame; the title is the only flexible cell, and nothing ever wraps.
243
+ */
244
+ export declare function renderConversationRow(row: ResumeTreeConversationRow, options: {
245
+ width: number;
246
+ selected: boolean;
247
+ now?: number;
248
+ columns?: ConversationColumns;
249
+ /** Show the conversation cwd as an absolute path instead of omitting it. */
250
+ fullPaths?: boolean;
251
+ }): string;
252
+ /**
253
+ * One workspace sub-header, indented one visible step: a coloured fold+name,
254
+ * a branch chip when it adds information, and right-aligned counts + `⋯`.
255
+ */
256
+ export declare function renderWorkspaceRow(row: ResumeTreeWorkspaceRow, options: {
257
+ width: number;
258
+ selected: boolean;
259
+ collapsed: boolean;
260
+ referenceBranch?: string | null;
261
+ current?: boolean;
262
+ /** Show absolute workspace paths instead of parent/name tails. */
263
+ fullPaths?: boolean;
264
+ /** Workspace scope has no use for the path label; callers can omit it. */
265
+ showPath?: boolean;
266
+ }): string;
267
+ /**
268
+ * Project section header: pin + name + path tag on the left, counts/workspaces/
269
+ * age right-aligned. The caller draws a full-width rule under it.
270
+ */
271
+ export declare function renderProjectRow(row: ResumeTreeProjectRow, options: {
272
+ width: number;
273
+ selected: boolean;
274
+ collapsed: boolean;
275
+ now?: number;
276
+ /** Show the absolute project root instead of the parent/name tail. */
277
+ fullPaths?: boolean;
278
+ }): string;
279
+ /** Full-width rule closing a project section header. */
280
+ export declare function renderProjectRule(width: number): string;
281
+ /** A scope-segment hit range on the header line, in tree-local columns. */
282
+ export interface ResumeScopeHit {
283
+ kind: ResumeTreeScope;
284
+ start: number;
285
+ end: number;
286
+ }
287
+ /** The three-tab scope control with per-scope counts, plus its clickable ranges. */
288
+ export declare function renderScopeHeaderHits(scope: ResumeTreeScope, summary: ResumeTreeScopeSummary, width: number): {
289
+ text: string;
290
+ hits: ResumeScopeHit[];
291
+ };
292
+ /** The three-tab scope control with per-scope counts. */
293
+ export declare function renderScopeHeader(scope: ResumeTreeScope, summary: ResumeTreeScopeSummary, width: number): string;
294
+ interface FlatRow {
295
+ row: ResumeTreeRow;
296
+ /** Visible when every ancestor group is expanded. */
297
+ parentKey?: string;
298
+ }
299
+ export interface ResumeTreeState {
300
+ collapsedProjects: Set<string>;
301
+ expandedProjects: Set<string>;
302
+ collapsedWorkspaces: Set<string>;
303
+ }
304
+ /** Flatten model rows under the current local collapse state (headers always stay). */
305
+ export declare function flattenResumeRows(rows: readonly ResumeTreeRow[], state: ResumeTreeState, searching: boolean): FlatRow[];
306
+ /**
307
+ * The ANSI resume tree component. The host mounts it inside its panel chrome;
308
+ * this component renders the scope control, search field, banners and rows.
309
+ */
310
+ export declare class ResumeTreeComponent implements Component {
311
+ #private;
312
+ readonly onSelect: (conversation: ResumeTreeConversation) => void;
313
+ readonly onCancel: () => void;
314
+ readonly onExit: () => void;
315
+ onRequestRender?: () => void;
316
+ constructor(options: ResumeTreeOptions);
317
+ /** Rebuild the cached view (call after the host refreshes its directory). */
318
+ invalidate(): void;
319
+ get scope(): ResumeTreeScope;
320
+ get fullPaths(): boolean;
321
+ /**
322
+ * Toggle absolute paths. In workspace scope the path is redundant, so the
323
+ * key surfaces a one-line hint and leaves the setting untouched; the
324
+ * project/all scopes actually flip and persist it.
325
+ */
326
+ toggleFullPaths(): void;
327
+ /** Adopt a host-refreshed view while keeping selection and collapse state. */
328
+ setView(view: ResumeTreeView | null): void;
329
+ /** Cycle workspace → project → all → workspace. */
330
+ cycleScope(direction?: 1 | -1): void;
331
+ /** Switch to a scope (the scope segments and Tab share this path). */
332
+ setScope(scope: ResumeTreeScope): void;
333
+ /** Re-read the directory through the host source. */
334
+ refresh(): Promise<void>;
335
+ /** Focus the search field (the `/` key, and the initial state). */
336
+ focusSearch(): void;
337
+ /** Pin the selected conversation (`p`) or its project (`P`). */
338
+ pinSelected(project: boolean): Promise<void>;
339
+ /**
340
+ * Hit-test an SGR mouse report against the rows the last render recorded.
341
+ * The fullscreen picker paints from screen row 0, so callers pass the
342
+ * component's frame offset (`row` of the first rendered line, `col` of its
343
+ * first content column). Keyboard handling never depends on this path.
344
+ */
345
+ handleMouse(data: string, offset?: {
346
+ row?: number;
347
+ col?: number;
348
+ }): boolean;
349
+ handleInput(keyData: string): void;
350
+ render(width: number): readonly string[];
351
+ dispose(): void;
352
+ }
353
+ export {};