@mariozechner/pi-coding-agent 0.48.0 → 0.49.1
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 +49 -0
- package/README.md +29 -2
- package/dist/core/agent-session.d.ts +2 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +19 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +11 -0
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +50 -3
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +4 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +4 -2
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +56 -24
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +23 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/model-registry.d.ts +2 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +38 -5
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/settings-manager.d.ts +3 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +7 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +16 -21
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/path-utils.d.ts +0 -1
- package/dist/core/tools/path-utils.d.ts.map +1 -1
- package/dist/core/tools/path-utils.js +0 -7
- package/dist/core/tools/path-utils.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +2 -12
- package/dist/core/tools/read.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/components/extension-input.d.ts +5 -2
- package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-input.js +9 -0
- package/dist/modes/interactive/components/extension-input.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts +5 -2
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +9 -0
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +5 -2
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +10 -1
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts +5 -2
- package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js +9 -0
- package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts +23 -5
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +327 -55
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +13 -1
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/tree-selector.d.ts +5 -2
- package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/tree-selector.js +23 -0
- package/dist/modes/interactive/components/tree-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +54 -6
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +16 -0
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +16 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/utils/image-convert.d.ts.map +1 -1
- package/dist/utils/image-convert.js +4 -3
- package/dist/utils/image-convert.js.map +1 -1
- package/dist/utils/image-resize.d.ts.map +1 -1
- package/dist/utils/image-resize.js +2 -2
- package/dist/utils/image-resize.js.map +1 -1
- package/dist/utils/photon.d.ts +6 -13
- package/dist/utils/photon.d.ts.map +1 -1
- package/dist/utils/photon.js +99 -29
- package/dist/utils/photon.js.map +1 -1
- package/docs/extensions.md +67 -1
- package/docs/session.md +6 -0
- package/docs/tui.md +30 -0
- package/examples/extensions/README.md +1 -0
- package/examples/extensions/custom-header.ts +2 -1
- package/examples/extensions/trigger-compact.ts +40 -0
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +5 -5
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { unlink } from "node:fs/promises";
|
|
1
4
|
import * as os from "node:os";
|
|
2
5
|
import { Container, getEditorKeybindings, Input, matchesKey, Spacer, truncateToWidth, visibleWidth, } from "@mariozechner/pi-tui";
|
|
3
6
|
import { theme } from "../theme/theme.js";
|
|
4
7
|
import { DynamicBorder } from "./dynamic-border.js";
|
|
8
|
+
import { keyHint, rawKeyHint } from "./keybinding-hints.js";
|
|
5
9
|
import { filterAndSortSessions } from "./session-selector-search.js";
|
|
6
10
|
function shortenPath(path) {
|
|
7
11
|
const home = os.homedir();
|
|
@@ -33,11 +37,17 @@ function formatSessionDate(date) {
|
|
|
33
37
|
class SessionSelectorHeader {
|
|
34
38
|
scope;
|
|
35
39
|
sortMode;
|
|
40
|
+
requestRender;
|
|
36
41
|
loading = false;
|
|
37
42
|
loadProgress = null;
|
|
38
|
-
|
|
43
|
+
showPath = false;
|
|
44
|
+
confirmingDeletePath = null;
|
|
45
|
+
statusMessage = null;
|
|
46
|
+
statusTimeout = null;
|
|
47
|
+
constructor(scope, sortMode, requestRender) {
|
|
39
48
|
this.scope = scope;
|
|
40
49
|
this.sortMode = sortMode;
|
|
50
|
+
this.requestRender = requestRender;
|
|
41
51
|
}
|
|
42
52
|
setScope(scope) {
|
|
43
53
|
this.scope = scope;
|
|
@@ -47,13 +57,35 @@ class SessionSelectorHeader {
|
|
|
47
57
|
}
|
|
48
58
|
setLoading(loading) {
|
|
49
59
|
this.loading = loading;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
60
|
+
// Progress is scoped to the current load; clear whenever the loading state is set
|
|
61
|
+
this.loadProgress = null;
|
|
53
62
|
}
|
|
54
63
|
setProgress(loaded, total) {
|
|
55
64
|
this.loadProgress = { loaded, total };
|
|
56
65
|
}
|
|
66
|
+
setShowPath(showPath) {
|
|
67
|
+
this.showPath = showPath;
|
|
68
|
+
}
|
|
69
|
+
setConfirmingDeletePath(path) {
|
|
70
|
+
this.confirmingDeletePath = path;
|
|
71
|
+
}
|
|
72
|
+
clearStatusTimeout() {
|
|
73
|
+
if (!this.statusTimeout)
|
|
74
|
+
return;
|
|
75
|
+
clearTimeout(this.statusTimeout);
|
|
76
|
+
this.statusTimeout = null;
|
|
77
|
+
}
|
|
78
|
+
setStatusMessage(msg, autoHideMs) {
|
|
79
|
+
this.clearStatusTimeout();
|
|
80
|
+
this.statusMessage = msg;
|
|
81
|
+
if (!msg || !autoHideMs)
|
|
82
|
+
return;
|
|
83
|
+
this.statusTimeout = setTimeout(() => {
|
|
84
|
+
this.statusMessage = null;
|
|
85
|
+
this.statusTimeout = null;
|
|
86
|
+
this.requestRender();
|
|
87
|
+
}, autoHideMs);
|
|
88
|
+
}
|
|
57
89
|
invalidate() { }
|
|
58
90
|
render(width) {
|
|
59
91
|
const title = this.scope === "current" ? "Resume Session (Current Folder)" : "Resume Session (All)";
|
|
@@ -65,20 +97,42 @@ class SessionSelectorHeader {
|
|
|
65
97
|
const progressText = this.loadProgress ? `${this.loadProgress.loaded}/${this.loadProgress.total}` : "...";
|
|
66
98
|
scopeText = `${theme.fg("muted", "○ Current Folder | ")}${theme.fg("accent", `Loading ${progressText}`)}`;
|
|
67
99
|
}
|
|
100
|
+
else if (this.scope === "current") {
|
|
101
|
+
scopeText = `${theme.fg("accent", "◉ Current Folder")}${theme.fg("muted", " | ○ All")}`;
|
|
102
|
+
}
|
|
68
103
|
else {
|
|
69
|
-
scopeText =
|
|
70
|
-
this.scope === "current"
|
|
71
|
-
? `${theme.fg("accent", "◉ Current Folder")}${theme.fg("muted", " | ○ All")}`
|
|
72
|
-
: `${theme.fg("muted", "○ Current Folder | ")}${theme.fg("accent", "◉ All")}`;
|
|
104
|
+
scopeText = `${theme.fg("muted", "○ Current Folder | ")}${theme.fg("accent", "◉ All")}`;
|
|
73
105
|
}
|
|
74
106
|
const rightText = truncateToWidth(`${scopeText} ${sortText}`, width, "");
|
|
75
107
|
const availableLeft = Math.max(0, width - visibleWidth(rightText) - 1);
|
|
76
108
|
const left = truncateToWidth(leftText, availableLeft, "");
|
|
77
109
|
const spacing = Math.max(0, width - visibleWidth(left) - visibleWidth(rightText));
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
110
|
+
// Build hint lines - changes based on state (all branches truncate to width)
|
|
111
|
+
let hintLine1;
|
|
112
|
+
let hintLine2;
|
|
113
|
+
if (this.confirmingDeletePath !== null) {
|
|
114
|
+
const confirmHint = "Delete session? [Enter] confirm · [Esc/Ctrl+C] cancel";
|
|
115
|
+
hintLine1 = theme.fg("error", truncateToWidth(confirmHint, width, "…"));
|
|
116
|
+
hintLine2 = "";
|
|
117
|
+
}
|
|
118
|
+
else if (this.statusMessage) {
|
|
119
|
+
const color = this.statusMessage.type === "error" ? "error" : "accent";
|
|
120
|
+
hintLine1 = theme.fg(color, truncateToWidth(this.statusMessage.message, width, "…"));
|
|
121
|
+
hintLine2 = "";
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
const pathState = this.showPath ? "(on)" : "(off)";
|
|
125
|
+
const sep = theme.fg("muted", " · ");
|
|
126
|
+
const hint1 = keyHint("tab", "scope") + sep + theme.fg("muted", 're:<pattern> regex · "phrase" exact');
|
|
127
|
+
const hint2 = rawKeyHint("ctrl+r", "sort") +
|
|
128
|
+
sep +
|
|
129
|
+
rawKeyHint("ctrl+d", "delete") +
|
|
130
|
+
sep +
|
|
131
|
+
rawKeyHint("ctrl+p", `path ${pathState}`);
|
|
132
|
+
hintLine1 = truncateToWidth(hint1, width, "…");
|
|
133
|
+
hintLine2 = truncateToWidth(hint2, width, "…");
|
|
134
|
+
}
|
|
135
|
+
return [`${left}${" ".repeat(spacing)}${rightText}`, hintLine1, hintLine2];
|
|
82
136
|
}
|
|
83
137
|
}
|
|
84
138
|
/**
|
|
@@ -91,18 +145,35 @@ class SessionList {
|
|
|
91
145
|
searchInput;
|
|
92
146
|
showCwd = false;
|
|
93
147
|
sortMode = "relevance";
|
|
148
|
+
showPath = false;
|
|
149
|
+
confirmingDeletePath = null;
|
|
150
|
+
currentSessionFilePath;
|
|
94
151
|
onSelect;
|
|
95
152
|
onCancel;
|
|
96
153
|
onExit = () => { };
|
|
97
154
|
onToggleScope;
|
|
98
155
|
onToggleSort;
|
|
99
|
-
|
|
100
|
-
|
|
156
|
+
onTogglePath;
|
|
157
|
+
onDeleteConfirmationChange;
|
|
158
|
+
onDeleteSession;
|
|
159
|
+
onError;
|
|
160
|
+
maxVisible = 5; // Max sessions visible (each session: message + metadata + optional path + blank)
|
|
161
|
+
// Focusable implementation - propagate to searchInput for IME cursor positioning
|
|
162
|
+
_focused = false;
|
|
163
|
+
get focused() {
|
|
164
|
+
return this._focused;
|
|
165
|
+
}
|
|
166
|
+
set focused(value) {
|
|
167
|
+
this._focused = value;
|
|
168
|
+
this.searchInput.focused = value;
|
|
169
|
+
}
|
|
170
|
+
constructor(sessions, showCwd, sortMode, currentSessionFilePath) {
|
|
101
171
|
this.allSessions = sessions;
|
|
102
172
|
this.filteredSessions = sessions;
|
|
103
173
|
this.searchInput = new Input();
|
|
104
174
|
this.showCwd = showCwd;
|
|
105
175
|
this.sortMode = sortMode;
|
|
176
|
+
this.currentSessionFilePath = currentSessionFilePath;
|
|
106
177
|
// Handle Enter in search input - select current item
|
|
107
178
|
this.searchInput.onSubmit = () => {
|
|
108
179
|
if (this.filteredSessions[this.selectedIndex]) {
|
|
@@ -126,6 +197,21 @@ class SessionList {
|
|
|
126
197
|
this.filteredSessions = filterAndSortSessions(this.allSessions, query, this.sortMode);
|
|
127
198
|
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredSessions.length - 1));
|
|
128
199
|
}
|
|
200
|
+
setConfirmingDeletePath(path) {
|
|
201
|
+
this.confirmingDeletePath = path;
|
|
202
|
+
this.onDeleteConfirmationChange?.(path);
|
|
203
|
+
}
|
|
204
|
+
startDeleteConfirmationForSelectedSession() {
|
|
205
|
+
const selected = this.filteredSessions[this.selectedIndex];
|
|
206
|
+
if (!selected)
|
|
207
|
+
return;
|
|
208
|
+
// Prevent deleting current session
|
|
209
|
+
if (this.currentSessionFilePath && selected.path === this.currentSessionFilePath) {
|
|
210
|
+
this.onError?.("Cannot delete the currently active session");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
this.setConfirmingDeletePath(selected.path);
|
|
214
|
+
}
|
|
129
215
|
invalidate() { }
|
|
130
216
|
render(width) {
|
|
131
217
|
const lines = [];
|
|
@@ -146,10 +232,11 @@ class SessionList {
|
|
|
146
232
|
// Calculate visible range with scrolling
|
|
147
233
|
const startIndex = Math.max(0, Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredSessions.length - this.maxVisible));
|
|
148
234
|
const endIndex = Math.min(startIndex + this.maxVisible, this.filteredSessions.length);
|
|
149
|
-
// Render visible sessions (
|
|
235
|
+
// Render visible sessions (message + metadata + optional path + blank line)
|
|
150
236
|
for (let i = startIndex; i < endIndex; i++) {
|
|
151
237
|
const session = this.filteredSessions[i];
|
|
152
238
|
const isSelected = i === this.selectedIndex;
|
|
239
|
+
const isConfirmingDelete = session.path === this.confirmingDeletePath;
|
|
153
240
|
// Use session name if set, otherwise first message
|
|
154
241
|
const hasName = !!session.name;
|
|
155
242
|
const displayText = session.name ?? session.firstMessage;
|
|
@@ -159,10 +246,14 @@ class SessionList {
|
|
|
159
246
|
const cursor = isSelected ? theme.fg("accent", "› ") : " ";
|
|
160
247
|
const maxMsgWidth = width - 2; // Account for cursor (2 visible chars)
|
|
161
248
|
const truncatedMsg = truncateToWidth(normalizedMessage, maxMsgWidth, "...");
|
|
162
|
-
let
|
|
163
|
-
if (
|
|
164
|
-
|
|
249
|
+
let messageColor = null;
|
|
250
|
+
if (isConfirmingDelete) {
|
|
251
|
+
messageColor = "error";
|
|
252
|
+
}
|
|
253
|
+
else if (hasName) {
|
|
254
|
+
messageColor = "warning";
|
|
165
255
|
}
|
|
256
|
+
let styledMsg = messageColor ? theme.fg(messageColor, truncatedMsg) : truncatedMsg;
|
|
166
257
|
if (isSelected) {
|
|
167
258
|
styledMsg = theme.bold(styledMsg);
|
|
168
259
|
}
|
|
@@ -175,9 +266,17 @@ class SessionList {
|
|
|
175
266
|
metadataParts.push(shortenPath(session.cwd));
|
|
176
267
|
}
|
|
177
268
|
const metadata = ` ${metadataParts.join(" · ")}`;
|
|
178
|
-
const
|
|
269
|
+
const truncatedMetadata = truncateToWidth(metadata, width, "");
|
|
270
|
+
const metadataLine = theme.fg(isConfirmingDelete ? "error" : "dim", truncatedMetadata);
|
|
179
271
|
lines.push(messageLine);
|
|
180
272
|
lines.push(metadataLine);
|
|
273
|
+
// Optional third line: file path (when showPath is enabled)
|
|
274
|
+
if (this.showPath) {
|
|
275
|
+
const pathText = ` ${shortenPath(session.path)}`;
|
|
276
|
+
const truncatedPath = truncateToWidth(pathText, width, "…");
|
|
277
|
+
const pathLine = theme.fg(isConfirmingDelete ? "error" : "muted", truncatedPath);
|
|
278
|
+
lines.push(pathLine);
|
|
279
|
+
}
|
|
181
280
|
lines.push(""); // Blank line between sessions
|
|
182
281
|
}
|
|
183
282
|
// Add scroll indicator if needed
|
|
@@ -190,6 +289,22 @@ class SessionList {
|
|
|
190
289
|
}
|
|
191
290
|
handleInput(keyData) {
|
|
192
291
|
const kb = getEditorKeybindings();
|
|
292
|
+
// Handle delete confirmation state first - intercept all keys
|
|
293
|
+
if (this.confirmingDeletePath !== null) {
|
|
294
|
+
if (kb.matches(keyData, "selectConfirm")) {
|
|
295
|
+
const pathToDelete = this.confirmingDeletePath;
|
|
296
|
+
this.setConfirmingDeletePath(null);
|
|
297
|
+
void this.onDeleteSession?.(pathToDelete);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
// Allow both Escape and Ctrl+C to cancel (consistent with pi UX)
|
|
301
|
+
if (kb.matches(keyData, "selectCancel") || matchesKey(keyData, "ctrl+c")) {
|
|
302
|
+
this.setConfirmingDeletePath(null);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
// Ignore all other keys while confirming
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
193
308
|
if (kb.matches(keyData, "tab")) {
|
|
194
309
|
if (this.onToggleScope) {
|
|
195
310
|
this.onToggleScope();
|
|
@@ -200,6 +315,28 @@ class SessionList {
|
|
|
200
315
|
this.onToggleSort?.();
|
|
201
316
|
return;
|
|
202
317
|
}
|
|
318
|
+
// Ctrl+P: toggle path display
|
|
319
|
+
if (matchesKey(keyData, "ctrl+p")) {
|
|
320
|
+
this.showPath = !this.showPath;
|
|
321
|
+
this.onTogglePath?.(this.showPath);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
// Ctrl+D: initiate delete confirmation (useful on terminals that don't distinguish Ctrl+Backspace from Backspace)
|
|
325
|
+
if (matchesKey(keyData, "ctrl+d")) {
|
|
326
|
+
this.startDeleteConfirmationForSelectedSession();
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
// Ctrl+Backspace: non-invasive convenience alias for delete
|
|
330
|
+
// Only triggers deletion when the query is empty; otherwise it is forwarded to the input
|
|
331
|
+
if (matchesKey(keyData, "ctrl+backspace")) {
|
|
332
|
+
if (this.searchInput.getValue().length > 0) {
|
|
333
|
+
this.searchInput.handleInput(keyData);
|
|
334
|
+
this.filterSessions(this.searchInput.getValue());
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
this.startDeleteConfirmationForSelectedSession();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
203
340
|
// Up arrow
|
|
204
341
|
if (kb.matches(keyData, "selectUp")) {
|
|
205
342
|
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
@@ -236,6 +373,42 @@ class SessionList {
|
|
|
236
373
|
}
|
|
237
374
|
}
|
|
238
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* Delete a session file, trying the `trash` CLI first, then falling back to unlink
|
|
378
|
+
*/
|
|
379
|
+
async function deleteSessionFile(sessionPath) {
|
|
380
|
+
// Try `trash` first (if installed)
|
|
381
|
+
const trashArgs = sessionPath.startsWith("-") ? ["--", sessionPath] : [sessionPath];
|
|
382
|
+
const trashResult = spawnSync("trash", trashArgs, { encoding: "utf-8" });
|
|
383
|
+
const getTrashErrorHint = () => {
|
|
384
|
+
const parts = [];
|
|
385
|
+
if (trashResult.error) {
|
|
386
|
+
parts.push(trashResult.error.message);
|
|
387
|
+
}
|
|
388
|
+
const stderr = trashResult.stderr?.trim();
|
|
389
|
+
if (stderr) {
|
|
390
|
+
parts.push(stderr.split("\n")[0] ?? stderr);
|
|
391
|
+
}
|
|
392
|
+
if (parts.length === 0)
|
|
393
|
+
return null;
|
|
394
|
+
return `trash: ${parts.join(" · ").slice(0, 200)}`;
|
|
395
|
+
};
|
|
396
|
+
// If trash reports success, or the file is gone afterwards, treat it as successful
|
|
397
|
+
if (trashResult.status === 0 || !existsSync(sessionPath)) {
|
|
398
|
+
return { ok: true, method: "trash" };
|
|
399
|
+
}
|
|
400
|
+
// Fallback to permanent deletion
|
|
401
|
+
try {
|
|
402
|
+
await unlink(sessionPath);
|
|
403
|
+
return { ok: true, method: "unlink" };
|
|
404
|
+
}
|
|
405
|
+
catch (err) {
|
|
406
|
+
const unlinkError = err instanceof Error ? err.message : String(err);
|
|
407
|
+
const trashErrorHint = getTrashErrorHint();
|
|
408
|
+
const error = trashErrorHint ? `${unlinkError} (${trashErrorHint})` : unlinkError;
|
|
409
|
+
return { ok: false, method: "unlink", error };
|
|
410
|
+
}
|
|
411
|
+
}
|
|
239
412
|
/**
|
|
240
413
|
* Component that renders a session selector
|
|
241
414
|
*/
|
|
@@ -250,26 +423,84 @@ export class SessionSelectorComponent extends Container {
|
|
|
250
423
|
allSessionsLoader;
|
|
251
424
|
onCancel;
|
|
252
425
|
requestRender;
|
|
253
|
-
|
|
426
|
+
currentLoading = false;
|
|
427
|
+
allLoading = false;
|
|
428
|
+
allLoadSeq = 0;
|
|
429
|
+
// Focusable implementation - propagate to sessionList for IME cursor positioning
|
|
430
|
+
_focused = false;
|
|
431
|
+
get focused() {
|
|
432
|
+
return this._focused;
|
|
433
|
+
}
|
|
434
|
+
set focused(value) {
|
|
435
|
+
this._focused = value;
|
|
436
|
+
this.sessionList.focused = value;
|
|
437
|
+
}
|
|
438
|
+
constructor(currentSessionsLoader, allSessionsLoader, onSelect, onCancel, onExit, requestRender, currentSessionFilePath) {
|
|
254
439
|
super();
|
|
255
440
|
this.currentSessionsLoader = currentSessionsLoader;
|
|
256
441
|
this.allSessionsLoader = allSessionsLoader;
|
|
257
442
|
this.onCancel = onCancel;
|
|
258
443
|
this.requestRender = requestRender;
|
|
259
|
-
this.header = new SessionSelectorHeader(this.scope, this.sortMode);
|
|
444
|
+
this.header = new SessionSelectorHeader(this.scope, this.sortMode, this.requestRender);
|
|
260
445
|
// Add header
|
|
261
446
|
this.addChild(new Spacer(1));
|
|
262
|
-
this.addChild(this.header);
|
|
263
|
-
this.addChild(new Spacer(1));
|
|
264
447
|
this.addChild(new DynamicBorder());
|
|
265
448
|
this.addChild(new Spacer(1));
|
|
449
|
+
this.addChild(this.header);
|
|
450
|
+
this.addChild(new Spacer(1));
|
|
266
451
|
// Create session list (starts empty, will be populated after load)
|
|
267
|
-
this.sessionList = new SessionList([], false, this.sortMode);
|
|
268
|
-
|
|
269
|
-
this.
|
|
270
|
-
this.sessionList.
|
|
452
|
+
this.sessionList = new SessionList([], false, this.sortMode, currentSessionFilePath);
|
|
453
|
+
// Ensure header status timeouts are cleared when leaving the selector
|
|
454
|
+
const clearStatusMessage = () => this.header.setStatusMessage(null);
|
|
455
|
+
this.sessionList.onSelect = (sessionPath) => {
|
|
456
|
+
clearStatusMessage();
|
|
457
|
+
onSelect(sessionPath);
|
|
458
|
+
};
|
|
459
|
+
this.sessionList.onCancel = () => {
|
|
460
|
+
clearStatusMessage();
|
|
461
|
+
onCancel();
|
|
462
|
+
};
|
|
463
|
+
this.sessionList.onExit = () => {
|
|
464
|
+
clearStatusMessage();
|
|
465
|
+
onExit();
|
|
466
|
+
};
|
|
271
467
|
this.sessionList.onToggleScope = () => this.toggleScope();
|
|
272
468
|
this.sessionList.onToggleSort = () => this.toggleSortMode();
|
|
469
|
+
// Sync list events to header
|
|
470
|
+
this.sessionList.onTogglePath = (showPath) => {
|
|
471
|
+
this.header.setShowPath(showPath);
|
|
472
|
+
this.requestRender();
|
|
473
|
+
};
|
|
474
|
+
this.sessionList.onDeleteConfirmationChange = (path) => {
|
|
475
|
+
this.header.setConfirmingDeletePath(path);
|
|
476
|
+
this.requestRender();
|
|
477
|
+
};
|
|
478
|
+
this.sessionList.onError = (msg) => {
|
|
479
|
+
this.header.setStatusMessage({ type: "error", message: msg }, 3000);
|
|
480
|
+
this.requestRender();
|
|
481
|
+
};
|
|
482
|
+
// Handle session deletion
|
|
483
|
+
this.sessionList.onDeleteSession = async (sessionPath) => {
|
|
484
|
+
const result = await deleteSessionFile(sessionPath);
|
|
485
|
+
if (result.ok) {
|
|
486
|
+
if (this.currentSessions) {
|
|
487
|
+
this.currentSessions = this.currentSessions.filter((s) => s.path !== sessionPath);
|
|
488
|
+
}
|
|
489
|
+
if (this.allSessions) {
|
|
490
|
+
this.allSessions = this.allSessions.filter((s) => s.path !== sessionPath);
|
|
491
|
+
}
|
|
492
|
+
const sessions = this.scope === "all" ? (this.allSessions ?? []) : (this.currentSessions ?? []);
|
|
493
|
+
const showCwd = this.scope === "all";
|
|
494
|
+
this.sessionList.setSessions(sessions, showCwd);
|
|
495
|
+
const msg = result.method === "trash" ? "Session moved to trash" : "Session deleted";
|
|
496
|
+
this.header.setStatusMessage({ type: "info", message: msg }, 2000);
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
const errorMessage = result.error ?? "Unknown error";
|
|
500
|
+
this.header.setStatusMessage({ type: "error", message: `Failed to delete: ${errorMessage}` }, 3000);
|
|
501
|
+
}
|
|
502
|
+
this.requestRender();
|
|
503
|
+
};
|
|
273
504
|
this.addChild(this.sessionList);
|
|
274
505
|
// Add bottom border
|
|
275
506
|
this.addChild(new Spacer(1));
|
|
@@ -278,16 +509,34 @@ export class SessionSelectorComponent extends Container {
|
|
|
278
509
|
this.loadCurrentSessions();
|
|
279
510
|
}
|
|
280
511
|
loadCurrentSessions() {
|
|
512
|
+
this.currentLoading = true;
|
|
513
|
+
this.header.setScope("current");
|
|
281
514
|
this.header.setLoading(true);
|
|
282
515
|
this.requestRender();
|
|
283
516
|
this.currentSessionsLoader((loaded, total) => {
|
|
517
|
+
if (this.scope !== "current")
|
|
518
|
+
return;
|
|
284
519
|
this.header.setProgress(loaded, total);
|
|
285
520
|
this.requestRender();
|
|
286
|
-
})
|
|
521
|
+
})
|
|
522
|
+
.then((sessions) => {
|
|
287
523
|
this.currentSessions = sessions;
|
|
524
|
+
this.currentLoading = false;
|
|
525
|
+
if (this.scope !== "current")
|
|
526
|
+
return;
|
|
288
527
|
this.header.setLoading(false);
|
|
289
528
|
this.sessionList.setSessions(sessions, false);
|
|
290
529
|
this.requestRender();
|
|
530
|
+
})
|
|
531
|
+
.catch((error) => {
|
|
532
|
+
this.currentLoading = false;
|
|
533
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
534
|
+
if (this.scope !== "current")
|
|
535
|
+
return;
|
|
536
|
+
this.header.setLoading(false);
|
|
537
|
+
this.header.setStatusMessage({ type: "error", message: `Failed to load sessions: ${message}` }, 4000);
|
|
538
|
+
this.sessionList.setSessions([], false);
|
|
539
|
+
this.requestRender();
|
|
291
540
|
});
|
|
292
541
|
}
|
|
293
542
|
toggleSortMode() {
|
|
@@ -298,39 +547,62 @@ export class SessionSelectorComponent extends Container {
|
|
|
298
547
|
}
|
|
299
548
|
toggleScope() {
|
|
300
549
|
if (this.scope === "current") {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
this.header.
|
|
305
|
-
this.sessionList.setSessions([], true); // Clear list while loading
|
|
306
|
-
this.requestRender();
|
|
307
|
-
// Load asynchronously with progress updates
|
|
308
|
-
this.allSessionsLoader((loaded, total) => {
|
|
309
|
-
this.header.setProgress(loaded, total);
|
|
310
|
-
this.requestRender();
|
|
311
|
-
}).then((sessions) => {
|
|
312
|
-
this.allSessions = sessions;
|
|
313
|
-
this.header.setLoading(false);
|
|
314
|
-
this.scope = "all";
|
|
315
|
-
this.sessionList.setSessions(this.allSessions, true);
|
|
316
|
-
this.requestRender();
|
|
317
|
-
// If no sessions in All scope either, cancel
|
|
318
|
-
if (this.allSessions.length === 0 && (this.currentSessions?.length ?? 0) === 0) {
|
|
319
|
-
this.onCancel();
|
|
320
|
-
}
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
else {
|
|
324
|
-
this.scope = "all";
|
|
550
|
+
this.scope = "all";
|
|
551
|
+
this.header.setScope(this.scope);
|
|
552
|
+
if (this.allSessions !== null) {
|
|
553
|
+
this.header.setLoading(false);
|
|
325
554
|
this.sessionList.setSessions(this.allSessions, true);
|
|
326
|
-
this.
|
|
555
|
+
this.requestRender();
|
|
556
|
+
return;
|
|
327
557
|
}
|
|
558
|
+
this.header.setLoading(true);
|
|
559
|
+
this.sessionList.setSessions([], true);
|
|
560
|
+
this.requestRender();
|
|
561
|
+
if (this.allLoading)
|
|
562
|
+
return;
|
|
563
|
+
this.allLoading = true;
|
|
564
|
+
const seq = ++this.allLoadSeq;
|
|
565
|
+
this.allSessionsLoader((loaded, total) => {
|
|
566
|
+
if (seq !== this.allLoadSeq)
|
|
567
|
+
return;
|
|
568
|
+
if (this.scope !== "all")
|
|
569
|
+
return;
|
|
570
|
+
this.header.setProgress(loaded, total);
|
|
571
|
+
this.requestRender();
|
|
572
|
+
})
|
|
573
|
+
.then((sessions) => {
|
|
574
|
+
this.allSessions = sessions;
|
|
575
|
+
this.allLoading = false;
|
|
576
|
+
if (seq !== this.allLoadSeq)
|
|
577
|
+
return;
|
|
578
|
+
if (this.scope !== "all")
|
|
579
|
+
return;
|
|
580
|
+
this.header.setLoading(false);
|
|
581
|
+
this.sessionList.setSessions(sessions, true);
|
|
582
|
+
this.requestRender();
|
|
583
|
+
if (sessions.length === 0 && (this.currentSessions?.length ?? 0) === 0) {
|
|
584
|
+
this.onCancel();
|
|
585
|
+
}
|
|
586
|
+
})
|
|
587
|
+
.catch((error) => {
|
|
588
|
+
this.allLoading = false;
|
|
589
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
590
|
+
if (seq !== this.allLoadSeq)
|
|
591
|
+
return;
|
|
592
|
+
if (this.scope !== "all")
|
|
593
|
+
return;
|
|
594
|
+
this.header.setLoading(false);
|
|
595
|
+
this.header.setStatusMessage({ type: "error", message: `Failed to load sessions: ${message}` }, 4000);
|
|
596
|
+
this.sessionList.setSessions([], true);
|
|
597
|
+
this.requestRender();
|
|
598
|
+
});
|
|
328
599
|
}
|
|
329
600
|
else {
|
|
330
|
-
// Switching back to "current"
|
|
331
601
|
this.scope = "current";
|
|
332
|
-
this.sessionList.setSessions(this.currentSessions ?? [], false);
|
|
333
602
|
this.header.setScope(this.scope);
|
|
603
|
+
this.header.setLoading(this.currentLoading);
|
|
604
|
+
this.sessionList.setSessions(this.currentSessions ?? [], false);
|
|
605
|
+
this.requestRender();
|
|
334
606
|
}
|
|
335
607
|
}
|
|
336
608
|
getSessionList() {
|