@mariozechner/pi-coding-agent 0.49.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +20 -1
  3. package/dist/core/extensions/runner.d.ts +2 -2
  4. package/dist/core/extensions/runner.d.ts.map +1 -1
  5. package/dist/core/extensions/runner.js +49 -24
  6. package/dist/core/extensions/runner.js.map +1 -1
  7. package/dist/core/model-registry.d.ts +2 -0
  8. package/dist/core/model-registry.d.ts.map +1 -1
  9. package/dist/core/model-registry.js +38 -5
  10. package/dist/core/model-registry.js.map +1 -1
  11. package/dist/modes/interactive/components/extension-input.d.ts +5 -2
  12. package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  13. package/dist/modes/interactive/components/extension-input.js +9 -0
  14. package/dist/modes/interactive/components/extension-input.js.map +1 -1
  15. package/dist/modes/interactive/components/login-dialog.d.ts +5 -2
  16. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  17. package/dist/modes/interactive/components/login-dialog.js +9 -0
  18. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  19. package/dist/modes/interactive/components/model-selector.d.ts +5 -2
  20. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  21. package/dist/modes/interactive/components/model-selector.js +10 -1
  22. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  23. package/dist/modes/interactive/components/scoped-models-selector.d.ts +5 -2
  24. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  25. package/dist/modes/interactive/components/scoped-models-selector.js +9 -0
  26. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  27. package/dist/modes/interactive/components/session-selector.d.ts +23 -5
  28. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  29. package/dist/modes/interactive/components/session-selector.js +327 -55
  30. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  31. package/dist/modes/interactive/components/tree-selector.d.ts +5 -2
  32. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  33. package/dist/modes/interactive/components/tree-selector.js +23 -0
  34. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  35. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  36. package/dist/modes/interactive/interactive-mode.js +6 -4
  37. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  38. package/dist/utils/photon.d.ts +3 -2
  39. package/dist/utils/photon.d.ts.map +1 -1
  40. package/dist/utils/photon.js +85 -3
  41. package/dist/utils/photon.js.map +1 -1
  42. package/docs/session.md +6 -0
  43. package/docs/tui.md +30 -0
  44. package/examples/extensions/with-deps/package-lock.json +2 -2
  45. package/examples/extensions/with-deps/package.json +1 -1
  46. 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
- constructor(scope, sortMode) {
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
- if (!loading) {
51
- this.loadProgress = null;
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
- const hintText = 'Tab: scope · Ctrl+R: sort · re:<pattern> for regex · "phrase" for exact phrase';
79
- const truncatedHint = truncateToWidth(hintText, width, "…");
80
- const hint = theme.fg("muted", truncatedHint);
81
- return [`${left}${" ".repeat(spacing)}${rightText}`, hint];
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
- maxVisible = 5; // Max sessions visible (each session is 3 lines: msg + metadata + blank)
100
- constructor(sessions, showCwd, sortMode) {
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 (2 lines per session + blank line)
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 styledMsg = truncatedMsg;
163
- if (hasName) {
164
- styledMsg = theme.fg("warning", truncatedMsg);
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 metadataLine = theme.fg("dim", truncateToWidth(metadata, width, ""));
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
- constructor(currentSessionsLoader, allSessionsLoader, onSelect, onCancel, onExit, requestRender) {
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
- this.sessionList.onSelect = onSelect;
269
- this.sessionList.onCancel = onCancel;
270
- this.sessionList.onExit = onExit;
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
- }).then((sessions) => {
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
- // Switching to "all" - load if not already loaded
302
- if (this.allSessions === null) {
303
- this.header.setLoading(true);
304
- this.header.setScope("all");
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.header.setScope(this.scope);
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() {