@timurproko/a1 0.2.5-dev.683 → 0.2.5-dev.704

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 (76) hide show
  1. package/dist/app/session-shell/prompt-suggestion-controller.d.ts +2 -0
  2. package/dist/app/session-shell/prompt-suggestion-controller.js +10 -0
  3. package/dist/app/session-shell/session-shell-root.js +15 -2
  4. package/dist/app/session-shell/session-shell.js +80 -23
  5. package/dist/app/session-shell/share-operation.d.ts +3 -0
  6. package/dist/app/session-shell/share-operation.js +11 -0
  7. package/dist/composition/owned-ui.js +2 -0
  8. package/dist/composition/settings-route-host.js +1 -1
  9. package/dist/contracts/presentation/index.d.ts +11 -1
  10. package/dist/contracts/presentation/index.js +6 -2
  11. package/dist/features/owned-ui/project-trust-dialog.js +7 -5
  12. package/dist/features/owned-ui/reference-screen-app.d.ts +2 -2
  13. package/dist/features/owned-ui/reference-screen-app.js +10 -6
  14. package/dist/features/owned-ui/settings-app.d.ts +1 -1
  15. package/dist/features/owned-ui/settings-app.js +342 -66
  16. package/dist/integrations/pi/components/index.d.ts +3 -2
  17. package/dist/integrations/pi/components/index.js +2 -1
  18. package/dist/integrations/pi/components/modal-frame.d.ts +11 -1
  19. package/dist/integrations/pi/components/modal-frame.js +29 -1
  20. package/dist/integrations/pi/components/models-dialog.js +19 -19
  21. package/dist/integrations/pi/components/share-operation-dialog.d.ts +6 -0
  22. package/dist/integrations/pi/components/share-operation-dialog.js +40 -0
  23. package/dist/integrations/pi/components/shell-editor-autocomplete.js +55 -8
  24. package/dist/integrations/pi/components/shell-extension-ui.d.ts +3 -0
  25. package/dist/integrations/pi/components/shell-extension-ui.js +8 -4
  26. package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +0 -4
  27. package/dist/integrations/pi/components/shell-selectors-dialogs.js +56 -34
  28. package/dist/integrations/pi/components/shell-session-info-reference.js +35 -2
  29. package/dist/integrations/pi/components/shell-shared-facade.d.ts +3 -1
  30. package/dist/integrations/pi/components/shell-shared-facade.js +13 -1
  31. package/dist/integrations/pi/components/skills-dialog.js +21 -8
  32. package/dist/integrations/pi/components/theme.d.ts +8 -1
  33. package/dist/integrations/pi/components/theme.js +28 -1
  34. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +10 -2
  35. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +20 -6
  36. package/dist/integrations/pi/components/upstream/components/extension-editor.js +2 -2
  37. package/dist/integrations/pi/components/upstream/components/extension-input.js +4 -5
  38. package/dist/integrations/pi/components/upstream/components/extension-selector.js +2 -3
  39. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +4 -2
  40. package/dist/integrations/pi/components/upstream/components/owned-editor.js +48 -12
  41. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +3 -2
  42. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +4 -15
  43. package/dist/integrations/pi/components/upstream/components/session-selector.d.ts +2 -0
  44. package/dist/integrations/pi/components/upstream/components/session-selector.js +137 -110
  45. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +9 -5
  46. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +65 -26
  47. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +31 -21
  48. package/dist/integrations/pi/components/upstream/components/tree-selector.js +255 -214
  49. package/dist/integrations/pi/components/upstream/components/trust-selector.js +13 -15
  50. package/dist/integrations/pi/engine/adapter.js +21 -2
  51. package/dist/integrations/pi/engine/workflow-runner.js +2 -1
  52. package/dist/native/darwin-arm64/manifest.json +1 -1
  53. package/dist/native/linux-x64/manifest.json +1 -1
  54. package/dist/native/win32-x64/manifest.json +2 -2
  55. package/dist/native/win32-x64/process-guardian.exe +0 -0
  56. package/dist/ui/apps/host.d.ts +4 -3
  57. package/dist/ui/apps/host.js +9 -4
  58. package/dist/ui/components/dialog-panel.d.ts +15 -0
  59. package/dist/ui/components/dialog-panel.js +55 -9
  60. package/dist/ui/components/index.d.ts +3 -3
  61. package/dist/ui/components/index.js +2 -2
  62. package/dist/ui/components/line-input.d.ts +2 -0
  63. package/dist/ui/components/line-input.js +2 -2
  64. package/dist/ui/components/list-view.d.ts +3 -3
  65. package/dist/ui/components/list-view.js +8 -9
  66. package/dist/ui/components/prompt-input.d.ts +2 -2
  67. package/dist/ui/components/prompt-input.js +5 -5
  68. package/dist/ui/components/shortcut-hints.d.ts +10 -3
  69. package/dist/ui/components/shortcut-hints.js +42 -6
  70. package/dist/ui/components/shortcuts.d.ts +1 -1
  71. package/dist/ui/components/shortcuts.js +4 -0
  72. package/dist/ui/components/theme.d.ts +1 -1
  73. package/dist/ui/components/value-menu.js +3 -1
  74. package/dist/ui/settings/manager.d.ts +1 -0
  75. package/dist/ui/settings/manager.js +9 -3
  76. package/package.json +1 -1
@@ -2,20 +2,26 @@
2
2
  * Provenance: @earendil-works/pi-coding-agent 1.0.4 (MIT), commit 7c10bd4337495ee613f2224843ecdf349b80d1df,
3
3
  * packages/coding-agent/src/modes/interactive/components/session-selector.ts.
4
4
  * Modifications: Source-synchronized session selector port: preserve threaded/current/all scope,
5
- * search, sort, named/path filters, rename, delete confirmation, active-session protection, loading
6
- * progress, cancellation, focus, and disposal while remapping public helpers, owned keybindings/theme,
7
- * canonical path handling, and the shared bare-A1 modal shortcut row and compact padded modal frame.
8
- * Deviations: owned-modal-shortcut-hints.
5
+ * search, sort, named/path filters, rename, delete confirmation, active-session protection,
6
+ * progressive partial results, cancellation, focus, and disposal while remapping public helpers, owned
7
+ * keybindings/theme, canonical path handling, reverse-Tab scope cycling, and the shared bare-A1 modal
8
+ * frame with standalone title, standard outer rules, stable filter/status row, progressive result
9
+ * paging, aligned result columns, Session Tree selection roles, quiet in-field query syntax guidance,
10
+ * and one-line bottom dynamic feedback and searchable-dialog shortcut footer. Selected session rows
11
+ * use the blue selection surface, success-green primary title, muted metadata, and ordinary text
12
+ * weight.
13
+ * Deviations: owned-modal-shortcut-hints, owned-resume-session-dialog,
14
+ * owned-standard-dialog-selection.
9
15
  */
10
16
  import { spawnSync } from "node:child_process";
11
17
  import { existsSync, realpathSync } from "node:fs";
12
18
  import { unlink } from "node:fs/promises";
13
19
  import * as os from "node:os";
14
- import { Container, getKeybindings, Input, Spacer, Text, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
20
+ import { Container, getKeybindings, Input, matchesKey, sliceByColumn, Spacer, Text, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
15
21
  import { KeybindingsManager } from "../adjacent/core/keybindings.js";
16
22
  import { DynamicBorder } from "../../../startup-public.js";
17
23
  import { addPiModalHeader, adoptPiModalFrame } from "../../modal-frame.js";
18
- import { piTheme, renderPiModalShortcutHints } from "../../theme.js";
24
+ import { DIALOG_CLOSE_SHORTCUT_HINT, piTheme, renderPiModalShortcutHints } from "../../theme.js";
19
25
  const theme = new Proxy({}, {
20
26
  get(_target, property) {
21
27
  const active = piTheme();
@@ -29,6 +35,16 @@ function keyText(keybinding, keybindings) {
29
35
  function shortcutHint(keybinding, action, keybindings) {
30
36
  return { key: keyText(keybinding, keybindings), action };
31
37
  }
38
+ /** Keep the canonical close control complete while clipping preceding state-specific guidance. */
39
+ function withCloseHint(content, width) {
40
+ const close = renderPiModalShortcutHints([DIALOG_CLOSE_SHORTCUT_HINT]);
41
+ const closeWidth = visibleWidth(close);
42
+ if (width <= closeWidth)
43
+ return truncateToWidth(close, width, "");
44
+ const bodyWidth = Math.max(0, width - closeWidth - 2);
45
+ const body = truncateToWidth(content, bodyWidth, "…");
46
+ return body.length === 0 ? close : `${body} ${close}`;
47
+ }
32
48
  import { filterAndSortSessions, hasSessionName } from "./session-selector-search.js";
33
49
  function shortenPath(path) {
34
50
  const home = os.homedir();
@@ -59,6 +75,27 @@ function formatSessionDate(date) {
59
75
  return `${Math.floor(diffDays / 30)}mo`;
60
76
  return `${Math.floor(diffDays / 365)}y`;
61
77
  }
78
+ function fitToWidth(value, width, ellipsis = "") {
79
+ const available = Math.max(0, width);
80
+ const clipped = truncateToWidth(value, available, ellipsis);
81
+ return `${clipped}${" ".repeat(Math.max(0, available - visibleWidth(clipped)))}`;
82
+ }
83
+ function fitPathToWidth(value, width, preserveTail) {
84
+ if (!preserveTail || visibleWidth(value) <= width)
85
+ return fitToWidth(value, width, "…");
86
+ const tailWidth = Math.max(0, width - 1);
87
+ const tail = sliceByColumn(value, Math.max(0, visibleWidth(value) - tailWidth), tailWidth, true);
88
+ return fitToWidth(`…${tail}`, width);
89
+ }
90
+ function renderSelectedRow(value) {
91
+ const marker = "\u0000";
92
+ const wrapper = theme.bg("selectedBg", marker);
93
+ const markerIndex = wrapper.indexOf(marker);
94
+ const on = wrapper.slice(0, markerIndex);
95
+ const off = wrapper.slice(markerIndex + marker.length);
96
+ const reasserted = value.replace(/\u001b\[[0-?]*[ -/]*m/gu, sequence => `${sequence}${on}`);
97
+ return `${on}${reasserted}${off}`;
98
+ }
62
99
  function canonicalizePath(path) {
63
100
  if (!path)
64
101
  return path;
@@ -75,8 +112,6 @@ class SessionSelectorHeader {
75
112
  nameFilter;
76
113
  requestRender;
77
114
  keybindings;
78
- loading = false;
79
- loadProgress = null;
80
115
  showPath = false;
81
116
  confirmingDeletePath = null;
82
117
  statusMessage = null;
@@ -98,14 +133,6 @@ class SessionSelectorHeader {
98
133
  setNameFilter(nameFilter) {
99
134
  this.nameFilter = nameFilter;
100
135
  }
101
- setLoading(loading) {
102
- this.loading = loading;
103
- // Progress is scoped to the current load; clear whenever the loading state is set
104
- this.loadProgress = null;
105
- }
106
- setProgress(loaded, total) {
107
- this.loadProgress = { loaded, total };
108
- }
109
136
  setShowPath(showPath) {
110
137
  this.showPath = showPath;
111
138
  }
@@ -134,64 +161,54 @@ class SessionSelectorHeader {
134
161
  }
135
162
  invalidate() { }
136
163
  render(width) {
137
- const title = this.scope === "current" ? "Resume Session (Current Folder)" : "Resume Session (All)";
138
- const leftText = theme.bold(title);
139
- const sortLabel = this.sortMode === "threaded" ? "Threaded" : this.sortMode === "recent" ? "Recent" : "Fuzzy";
164
+ const title = theme.fg("accent", theme.bold("Resume Session"));
165
+ const scopeText = theme.fg("muted", "Filter: ")
166
+ + theme.fg(this.scope === "current" ? "accent" : "dim", "current")
167
+ + theme.fg("muted", " | ")
168
+ + theme.fg(this.scope === "all" ? "accent" : "dim", "all");
169
+ const nameText = theme.fg("muted", "Name: ") + theme.fg("accent", this.nameFilter);
170
+ const sortLabel = this.sortMode === "relevance" ? "fuzzy" : this.sortMode;
140
171
  const sortText = theme.fg("muted", "Sort: ") + theme.fg("accent", sortLabel);
141
- const nameLabel = this.nameFilter === "all" ? "All" : "Named";
142
- const nameText = theme.fg("muted", "Name: ") + theme.fg("accent", nameLabel);
143
- let scopeText;
144
- if (this.loading) {
145
- const progressText = this.loadProgress ? `${this.loadProgress.loaded}/${this.loadProgress.total}` : "...";
146
- scopeText = `${theme.fg("muted", "○ Current Folder | ")}${theme.fg("accent", `Loading ${progressText}`)}`;
147
- }
148
- else if (this.scope === "current") {
149
- scopeText = `${theme.fg("accent", "◉ Current Folder")}${theme.fg("muted", " | ○ All")}`;
150
- }
151
- else {
152
- scopeText = `${theme.fg("muted", "○ Current Folder | ")}${theme.fg("accent", "◉ All")}`;
153
- }
154
- const rightText = truncateToWidth(`${scopeText} ${nameText} ${sortText}`, width, "");
155
- const availableLeft = Math.max(0, width - visibleWidth(rightText) - 1);
156
- const left = truncateToWidth(leftText, availableLeft, "");
157
- const spacing = Math.max(0, width - visibleWidth(left) - visibleWidth(rightText));
158
- // Build hint lines - changes based on state (all branches truncate to width)
159
- let hintLine1;
160
- let hintLine2;
172
+ const status = `${scopeText} ${nameText} ${sortText}`;
173
+ return [truncateToWidth(title, width, ""), truncateToWidth(status, width, "")];
174
+ }
175
+ renderFooter(width) {
161
176
  if (this.confirmingDeletePath !== null) {
162
177
  const confirmHint = theme.fg("error", "Delete session? ") + renderPiModalShortcutHints([
163
178
  shortcutHint("tui.select.confirm", "confirm", this.keybindings),
164
- shortcutHint("tui.select.cancel", "cancel", this.keybindings),
165
179
  ]);
166
- hintLine1 = truncateToWidth(confirmHint, width, "…");
167
- hintLine2 = "";
180
+ return [withCloseHint(confirmHint, width)];
168
181
  }
169
- else if (this.statusMessage) {
182
+ if (this.statusMessage) {
170
183
  const color = this.statusMessage.type === "error" ? "error" : "accent";
171
- hintLine1 = theme.fg(color, truncateToWidth(this.statusMessage.message, width, "…"));
172
- hintLine2 = "";
173
- }
174
- else {
175
- const pathState = this.showPath ? "(on)" : "(off)";
176
- const hint1 = renderPiModalShortcutHints([
177
- shortcutHint("tui.input.tab", "scope", this.keybindings),
178
- { action: "re:<pattern> regex" },
179
- { action: '"phrase" exact' },
180
- ]);
181
- const hint2Parts = [
182
- shortcutHint("app.session.toggleSort", "sort", this.keybindings),
183
- shortcutHint("app.session.toggleNamedFilter", "named", this.keybindings),
184
- shortcutHint("app.session.delete", "delete", this.keybindings),
185
- shortcutHint("app.session.togglePath", `path ${pathState}`, this.keybindings),
186
- ];
187
- if (this.showRenameHint) {
188
- hint2Parts.push(shortcutHint("app.session.rename", "rename", this.keybindings));
189
- }
190
- const hint2 = renderPiModalShortcutHints(hint2Parts);
191
- hintLine1 = truncateToWidth(hint1, width, "…");
192
- hintLine2 = truncateToWidth(hint2, width, "…");
193
- }
194
- return [`${left}${" ".repeat(spacing)}${rightText}`, hintLine1, hintLine2];
184
+ return [withCloseHint(theme.fg(color, this.statusMessage.message), width)];
185
+ }
186
+ const pathState = this.showPath ? "(on)" : "(off)";
187
+ const hints = [
188
+ { key: "Type", action: "search" },
189
+ { key: "↑↓", action: "navigate" },
190
+ shortcutHint("tui.select.confirm", "select", this.keybindings),
191
+ shortcutHint("tui.input.tab", "scope", this.keybindings),
192
+ shortcutHint("app.session.toggleSort", "sort", this.keybindings),
193
+ shortcutHint("app.session.toggleNamedFilter", "named", this.keybindings),
194
+ shortcutHint("app.session.delete", "delete", this.keybindings),
195
+ shortcutHint("app.session.togglePath", `path ${pathState}`, this.keybindings),
196
+ ];
197
+ if (this.showRenameHint)
198
+ hints.push(shortcutHint("app.session.rename", "rename", this.keybindings));
199
+ return [withCloseHint(renderPiModalShortcutHints(hints), width)];
200
+ }
201
+ }
202
+ class SessionSelectorFooter {
203
+ presentation;
204
+ constructor(presentation) {
205
+ this.presentation = presentation;
206
+ }
207
+ invalidate() {
208
+ this.presentation.invalidate();
209
+ }
210
+ render(width) {
211
+ return this.presentation.renderFooter(width);
195
212
  }
196
213
  }
197
214
  /**
@@ -300,7 +317,10 @@ class SessionList {
300
317
  constructor(sessions, showCwd, sortMode, nameFilter, keybindings, currentSessionFilePath) {
301
318
  this.allSessions = sessions;
302
319
  this.filteredSessions = [];
303
- this.searchInput = new Input();
320
+ this.searchInput = new Input({
321
+ placeholder: 're:<pattern> regex, "phrase" exact',
322
+ placeholderStyle: text => text === "r" ? text : `\u001b[2m${text}\u001b[22m`,
323
+ });
304
324
  this.showCwd = showCwd;
305
325
  this.sortMode = sortMode;
306
326
  this.nameFilter = nameFilter;
@@ -379,6 +399,11 @@ class SessionList {
379
399
  return false;
380
400
  return (canonicalizePath(path) ?? path) === this.currentSessionCanonicalPath;
381
401
  }
402
+ sessionPathText(session) {
403
+ if (this.showPath)
404
+ return shortenPath(session.path);
405
+ return this.showCwd && session.cwd ? shortenPath(session.cwd) : "";
406
+ }
382
407
  invalidate() { }
383
408
  render(width) {
384
409
  const lines = [];
@@ -410,7 +435,18 @@ class SessionList {
410
435
  // Calculate visible range with scrolling
411
436
  const startIndex = Math.max(0, Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredSessions.length - this.maxVisible));
412
437
  const endIndex = Math.min(startIndex + this.maxVisible, this.filteredSessions.length);
413
- // Render visible sessions (one line each with tree structure)
438
+ const pathTexts = this.filteredSessions.map((node) => this.sessionPathText(node.session));
439
+ const longestPath = Math.max(0, ...pathTexts.map((path) => visibleWidth(path)));
440
+ const countColumnWidth = Math.max(1, ...this.filteredSessions.map((node) => String(node.session.messageCount).length));
441
+ const ageColumnWidth = Math.max(1, ...this.filteredSessions.map((node) => formatSessionDate(node.session.modified).length));
442
+ const trailingColumnsWidth = countColumnWidth + 1 + ageColumnWidth;
443
+ const columnGapWidth = 2;
444
+ const maxPathColumnWidth = Math.max(0, width - trailingColumnsWidth - columnGapWidth * 2 - 12);
445
+ const pathColumnWidth = Math.min(longestPath, Math.floor(width * 0.45), maxPathColumnWidth);
446
+ const metadataWidth = columnGapWidth + trailingColumnsWidth
447
+ + (pathColumnWidth > 0 ? pathColumnWidth + columnGapWidth : 0);
448
+ const titleColumnWidth = Math.max(0, width - metadataWidth);
449
+ // Render visible sessions (one line each with stable title, path, count, and age columns)
414
450
  for (let i = startIndex; i < endIndex; i++) {
415
451
  const node = this.filteredSessions[i];
416
452
  const session = node.session;
@@ -423,23 +459,14 @@ class SessionList {
423
459
  const hasName = !!session.name;
424
460
  const displayText = session.name ?? session.firstMessage;
425
461
  const normalizedMessage = displayText.replace(/[\x00-\x1f\x7f]/g, " ").trim();
426
- // Right side: message count and age
427
462
  const age = formatSessionDate(session.modified);
428
463
  const msgCount = String(session.messageCount);
429
- let rightPart = `${msgCount} ${age}`;
430
- if (this.showCwd && session.cwd) {
431
- rightPart = `${shortenPath(session.cwd)} ${rightPart}`;
432
- }
433
- if (this.showPath) {
434
- rightPart = `${shortenPath(session.path)} ${rightPart}`;
435
- }
436
- // Cursor
437
- const cursor = isSelected ? theme.fg("accent", "› ") : " ";
438
- // Calculate available width for message
464
+ const pathText = this.sessionPathText(session);
465
+ // Cursor and title stay inside their column so metadata always begins at one boundary.
466
+ const cursor = isSelected ? theme.fg("accent", "→ ") : " ";
439
467
  const prefixWidth = visibleWidth(prefix);
440
- const rightWidth = visibleWidth(rightPart) + 2; // +2 for spacing
441
- const availableForMsg = width - 2 - prefixWidth - rightWidth; // -2 for cursor
442
- const truncatedMsg = truncateToWidth(normalizedMessage, Math.max(10, availableForMsg), "…");
468
+ const availableForMsg = Math.max(0, titleColumnWidth - 2 - prefixWidth);
469
+ const truncatedMsg = truncateToWidth(normalizedMessage, availableForMsg, "…");
443
470
  // Style message
444
471
  let messageColor = null;
445
472
  if (isConfirmingDelete) {
@@ -451,20 +478,22 @@ class SessionList {
451
478
  else if (hasName) {
452
479
  messageColor = "warning";
453
480
  }
454
- let styledMsg = messageColor ? theme.fg(messageColor, truncatedMsg) : truncatedMsg;
455
- if (isSelected) {
456
- styledMsg = theme.bold(styledMsg);
457
- }
458
- // Build line
459
- const leftPart = cursor + theme.fg("dim", prefix) + styledMsg;
460
- const leftWidth = visibleWidth(leftPart);
461
- const spacing = Math.max(1, width - leftWidth - visibleWidth(rightPart));
462
- const styledRight = theme.fg(isConfirmingDelete ? "error" : "dim", rightPart);
463
- let line = leftPart + " ".repeat(spacing) + styledRight;
464
- if (isSelected) {
465
- line = theme.bg("selectedBg", line);
466
- }
467
- lines.push(truncateToWidth(line, width));
481
+ const styledMsg = isConfirmingDelete
482
+ ? theme.fg("error", truncatedMsg)
483
+ : isSelected
484
+ ? theme.fg("success", truncatedMsg)
485
+ : messageColor
486
+ ? theme.fg(messageColor, truncatedMsg)
487
+ : truncatedMsg;
488
+ const metadataColor = isConfirmingDelete ? "error" : isSelected ? "muted" : "dim";
489
+ const titleColumn = fitToWidth(cursor + theme.fg("dim", prefix) + styledMsg, titleColumnWidth, "…");
490
+ const pathColumn = pathColumnWidth > 0
491
+ ? `${theme.fg(metadataColor, fitPathToWidth(pathText, pathColumnWidth, this.showPath))}${" ".repeat(columnGapWidth)}`
492
+ : "";
493
+ const countColumn = theme.fg(metadataColor, msgCount.padStart(countColumnWidth));
494
+ const ageColumn = theme.fg(metadataColor, age.padStart(ageColumnWidth));
495
+ const fittedLine = fitToWidth(`${titleColumn}${" ".repeat(columnGapWidth)}${pathColumn}${countColumn} ${ageColumn}`, width);
496
+ lines.push(isSelected ? renderSelectedRow(fittedLine) : fittedLine);
468
497
  }
469
498
  // Add scroll indicator if needed
470
499
  if (startIndex > 0 || endIndex < this.filteredSessions.length) {
@@ -499,7 +528,7 @@ class SessionList {
499
528
  // Ignore all other keys while confirming
500
529
  return;
501
530
  }
502
- if (kb.matches(keyData, "tui.input.tab")) {
531
+ if (matchesKey(keyData, "shift+tab") || kb.matches(keyData, "tui.input.tab")) {
503
532
  if (this.onToggleScope) {
504
533
  this.onToggleScope();
505
534
  }
@@ -635,6 +664,7 @@ export class SessionSelectorComponent extends Container {
635
664
  canRename = true;
636
665
  sessionList;
637
666
  header;
667
+ footer;
638
668
  keybindings;
639
669
  scope = "current";
640
670
  sortMode = "threaded";
@@ -665,18 +695,19 @@ export class SessionSelectorComponent extends Container {
665
695
  }
666
696
  buildBaseLayout(content, options) {
667
697
  this.clear();
668
- this.addChild(new Spacer(1));
669
698
  const showHeader = options?.showHeader ?? true;
670
699
  const modalHeader = showHeader
671
- ? addPiModalHeader(this, new DynamicBorder((s) => theme.fg("accent", s)), this.header)
700
+ ? addPiModalHeader(this, new DynamicBorder(), this.header)
672
701
  : undefined;
673
702
  if (!showHeader)
674
- this.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
703
+ this.addChild(new DynamicBorder());
675
704
  this.addChild(new Spacer(1));
676
705
  this.addChild(content);
677
706
  this.addChild(new Spacer(1));
678
- this.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
679
- const frame = { topIndex: 1, bottomIndex: this.children.length - 1 };
707
+ if (showHeader)
708
+ this.addChild(this.footer);
709
+ this.addChild(new DynamicBorder());
710
+ const frame = { topIndex: 0, bottomIndex: this.children.length - 1 };
680
711
  if (modalHeader === undefined)
681
712
  adoptPiModalFrame(this, frame);
682
713
  else
@@ -689,6 +720,7 @@ export class SessionSelectorComponent extends Container {
689
720
  this.allSessionsLoader = allSessionsLoader;
690
721
  this.requestRender = requestRender;
691
722
  this.header = new SessionSelectorHeader(this.scope, this.sortMode, this.nameFilter, this.requestRender, this.keybindings);
723
+ this.footer = new SessionSelectorFooter(this.header);
692
724
  const renameSession = options?.renameSession;
693
725
  this.renameSession = renameSession;
694
726
  this.canRename = !!renameSession;
@@ -791,7 +823,7 @@ export class SessionSelectorComponent extends Container {
791
823
  panel.addChild(new Spacer(1));
792
824
  panel.addChild(new Text(renderPiModalShortcutHints([
793
825
  shortcutHint("tui.select.confirm", "to save", this.keybindings),
794
- shortcutHint("tui.select.cancel", "to cancel", this.keybindings),
826
+ DIALOG_CLOSE_SHORTCUT_HINT,
795
827
  ]), 1, 0));
796
828
  this.buildBaseLayout(panel, { showHeader: false });
797
829
  this.requestRender();
@@ -837,10 +869,9 @@ export class SessionSelectorComponent extends Container {
837
869
  this.allLoad = controller;
838
870
  }
839
871
  this.header.setScope(scope);
840
- this.header.setLoading(true);
841
872
  this.requestRender();
842
873
  const isActive = () => (scope === "current" ? this.currentLoad : this.allLoad) === controller;
843
- const onProgress = (loaded, total, partialSessions) => {
874
+ const onProgress = (_loaded, _total, partialSessions) => {
844
875
  if (!isActive())
845
876
  return;
846
877
  if (partialSessions) {
@@ -856,7 +887,6 @@ export class SessionSelectorComponent extends Container {
856
887
  }
857
888
  if (scope !== this.scope)
858
889
  return;
859
- this.header.setProgress(loaded, total);
860
890
  this.requestRender();
861
891
  };
862
892
  try {
@@ -875,7 +905,6 @@ export class SessionSelectorComponent extends Container {
875
905
  }
876
906
  if (scope !== this.scope)
877
907
  return;
878
- this.header.setLoading(false);
879
908
  this.sessionList.setSessions(sessions, showCwd);
880
909
  this.requestRender();
881
910
  }
@@ -893,7 +922,6 @@ export class SessionSelectorComponent extends Container {
893
922
  if (scope !== this.scope)
894
923
  return;
895
924
  const message = err instanceof Error ? err.message : String(err);
896
- this.header.setLoading(false);
897
925
  this.header.setStatusMessage({ type: "error", message: `Failed to load sessions: ${message}` }, 4000);
898
926
  this.sessionList.setSessions([], showCwd);
899
927
  this.requestRender();
@@ -923,7 +951,6 @@ export class SessionSelectorComponent extends Container {
923
951
  const sessions = this.scope === "current" ? this.currentSessions : this.allSessions;
924
952
  const loading = (this.scope === "current" ? this.currentLoad : this.allLoad) !== null;
925
953
  this.header.setScope(this.scope);
926
- this.header.setLoading(loading);
927
954
  this.sessionList.setSessions(sessions ?? [], this.scope === "all");
928
955
  this.requestRender();
929
956
  if (sessions === null && !loading)
@@ -5,12 +5,16 @@
5
5
  * selection, and focus while accepting the active bare-A1 cycle-key label from the shell, styling the
6
6
  * title with the established bold semantic accent treatment, placing it directly below the top rule
7
7
  * through the shared compact padded modal frame and its muted hint directly below it, deduplicating
8
- * levels, and rendering aligned muted descriptions after fixed active and bracketed default columns.
9
- * Bare A1 persists defaults immediately on Space, closes only on Escape, and uses the shared compact
10
- * semantic shortcut row. All list and border colors use the owned theme and its explicit color mode.
11
- * The comparison profile retains the public pinned component.
8
+ * levels, and rendering exclusive radio-style default markers before level names, an item-adjacent
9
+ * active checkmark, and aligned muted descriptions. Bare A1 persists defaults immediately on Space,
10
+ * closes through the implicit Ctrl+C-capable selection-cancel action, and uses the shared compact
11
+ * semantic shortcut row with Type/search guidance without advertising that alias. All list and border
12
+ * colors use the owned theme and its explicit color mode. Selected rows use the shared bare-A1 blue
13
+ * selection surface only around the rendered item span, with the existing accent arrow, normal-text
14
+ * level, and preserved current/default/description roles. The comparison profile retains the public
15
+ * pinned component.
12
16
  * Deviations: owned-modal-shortcut-hints, owned-level-cycle-shortcut, owned-thinking-selector-heading,
13
- * owned-thinking-selector-controls.
17
+ * owned-thinking-selector-controls, owned-dialog-ctrl-c-cancel, owned-standard-dialog-selection.
14
18
  */
15
19
  import { Container, type Focusable, SelectList } from "@earendil-works/pi-tui";
16
20
  export type ThinkingSelectorLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
@@ -5,17 +5,21 @@
5
5
  * selection, and focus while accepting the active bare-A1 cycle-key label from the shell, styling the
6
6
  * title with the established bold semantic accent treatment, placing it directly below the top rule
7
7
  * through the shared compact padded modal frame and its muted hint directly below it, deduplicating
8
- * levels, and rendering aligned muted descriptions after fixed active and bracketed default columns.
9
- * Bare A1 persists defaults immediately on Space, closes only on Escape, and uses the shared compact
10
- * semantic shortcut row. All list and border colors use the owned theme and its explicit color mode.
11
- * The comparison profile retains the public pinned component.
8
+ * levels, and rendering exclusive radio-style default markers before level names, an item-adjacent
9
+ * active checkmark, and aligned muted descriptions. Bare A1 persists defaults immediately on Space,
10
+ * closes through the implicit Ctrl+C-capable selection-cancel action, and uses the shared compact
11
+ * semantic shortcut row with Type/search guidance without advertising that alias. All list and border
12
+ * colors use the owned theme and its explicit color mode. Selected rows use the shared bare-A1 blue
13
+ * selection surface only around the rendered item span, with the existing accent arrow, normal-text
14
+ * level, and preserved current/default/description roles. The comparison profile retains the public
15
+ * pinned component.
12
16
  * Deviations: owned-modal-shortcut-hints, owned-level-cycle-shortcut, owned-thinking-selector-heading,
13
- * owned-thinking-selector-controls.
17
+ * owned-thinking-selector-controls, owned-dialog-ctrl-c-cancel, owned-standard-dialog-selection.
14
18
  */
15
- import { Container, fuzzyFilter, getKeybindings, Input, Key, matchesKey, SelectList, Spacer, Text, } from "@earendil-works/pi-tui";
19
+ import { Container, fuzzyFilter, getKeybindings, Input, SelectList, Spacer, Text, } from "@earendil-works/pi-tui";
16
20
  import { DynamicBorder } from "../../../startup-public.js";
17
21
  import { addPiModalHeader, adoptPiModalFrame } from "../../modal-frame.js";
18
- import { piTheme, renderPiModalShortcutHints } from "../../theme.js";
22
+ import { DIALOG_CLOSE_SHORTCUT_HINT, piTheme, renderPiModalListRow, renderPiModalShortcutHints } from "../../theme.js";
19
23
  const THINKING_SELECT_LIST_LAYOUT = {
20
24
  minPrimaryColumnWidth: 12,
21
25
  maxPrimaryColumnWidth: 32,
@@ -29,6 +33,25 @@ const LEVEL_DESCRIPTIONS = {
29
33
  xhigh: "Extra-high reasoning (~32k tokens)",
30
34
  max: "Maximum reasoning",
31
35
  };
36
+ class ThinkingSelectListPresentation {
37
+ list;
38
+ items;
39
+ constructor(list, items) {
40
+ this.list = list;
41
+ this.items = items;
42
+ }
43
+ invalidate() {
44
+ this.list.invalidate();
45
+ }
46
+ handleMouse(event) {
47
+ return this.list.handleMouse(event);
48
+ }
49
+ render(width) {
50
+ const selected = this.list.getSelectedItem();
51
+ const selectedIndex = selected === null ? -1 : this.items.findIndex((item) => item.value === selected.value);
52
+ return this.list.render(width).map((row, index) => renderPiModalListRow(row, width, index === selectedIndex));
53
+ }
54
+ }
32
55
  /** Bare-A1 thinking-level selector with profile-resolved shortcut presentation. */
33
56
  export class OwnedThinkingSelectorComponent extends Container {
34
57
  searchInput;
@@ -67,15 +90,17 @@ export class OwnedThinkingSelectorComponent extends Container {
67
90
  this.searchInput.onSubmit = () => this.selectList.handleInput("\r");
68
91
  this.addChild(this.searchInput);
69
92
  this.addChild(new Spacer(1));
70
- this.selectList = this.buildSelectList(this.allItems, currentLevel);
93
+ const initialList = this.buildSelectList(this.allItems, currentLevel);
94
+ this.selectList = initialList.list;
71
95
  this.selectListContainer = new Container();
72
- this.selectListContainer.addChild(this.selectList);
96
+ this.selectListContainer.addChild(initialList.presentation);
73
97
  this.addChild(this.selectListContainer);
74
98
  this.addChild(new Spacer(1));
75
99
  this.addChild(new Text(renderPiModalShortcutHints([
100
+ { key: "Type", action: "search" },
76
101
  { key: this.keyDisplayText("tui.select.confirm"), action: "select" },
77
102
  { key: "Space", action: "default" },
78
- { key: "Esc", action: "close" },
103
+ DIALOG_CLOSE_SHORTCUT_HINT,
79
104
  ]), 0, 0));
80
105
  this.addChild(new DynamicBorder((text) => piTheme().fg("border", text)));
81
106
  adoptPiModalFrame(this, { topIndex: 0, bottomIndex: this.children.length - 1, header });
@@ -89,29 +114,43 @@ export class OwnedThinkingSelectorComponent extends Container {
89
114
  .join("/");
90
115
  }
91
116
  buildSelectList(items, preselect) {
117
+ const theme = piTheme();
92
118
  const levelWidth = this.allItems.reduce((widest, item) => Math.max(widest, (item.label ?? item.value).length), 0);
119
+ const selectedDefaultMarker = theme.fg("accent", "◉");
120
+ const unselectedDefaultMarker = theme.fg("dim", "○");
93
121
  const themedItems = items.map((item) => {
94
- const level = (item.label ?? item.value).padEnd(levelWidth);
95
- const currentMarker = item.value === this.currentLevel ? ` ${piTheme().fg("success", "✓")}` : " ";
96
- const defaultMarker = item.value === this.defaultThinkingLevel
97
- ? ` ${piTheme().fg("muted", "[default]")}`
98
- : " ";
99
- const description = item.description ? piTheme().fg("muted", item.description) : "";
100
- return { value: item.value, label: `${level}${currentMarker}${defaultMarker} ${description}` };
122
+ const level = item.label ?? item.value;
123
+ const isCurrent = item.value === this.currentLevel;
124
+ const currentMarker = isCurrent ? ` ${theme.fg("success", "✓")}` : "";
125
+ const statePadding = " ".repeat(levelWidth - level.length + (isCurrent ? 0 : 2));
126
+ const defaultMarker = item.value === this.defaultThinkingLevel ? selectedDefaultMarker : unselectedDefaultMarker;
127
+ const description = item.description ? theme.fg("muted", item.description) : "";
128
+ return { value: item.value, label: `${defaultMarker} ${level}${currentMarker}${statePadding} ${description}` };
101
129
  });
102
130
  // Invariant: the bare selector uses the owned theme's explicit color mode, not upstream's host detection.
103
131
  const list = new SelectList(themedItems, Math.max(1, themedItems.length), {
104
- selectedPrefix: text => piTheme().fg("accent", text),
105
- selectedText: text => piTheme().fg("accent", text),
106
- description: text => piTheme().fg("muted", text),
107
- scrollInfo: text => piTheme().fg("muted", text),
108
- noMatch: text => piTheme().fg("muted", text),
132
+ selectedPrefix: text => theme.fg("accent", text),
133
+ selectedText: text => {
134
+ const arrowEnd = text.startsWith("→ ") ? 2 : 0;
135
+ const arrow = arrowEnd === 0 ? "" : theme.fg("accent", text.slice(0, arrowEnd));
136
+ const marker = text.includes(selectedDefaultMarker) ? selectedDefaultMarker : unselectedDefaultMarker;
137
+ const markerIndex = text.indexOf(marker, arrowEnd);
138
+ return markerIndex === -1
139
+ ? arrow + theme.fg("text", text.slice(arrowEnd))
140
+ : arrow
141
+ + theme.fg("text", text.slice(arrowEnd, markerIndex))
142
+ + marker
143
+ + theme.fg("text", text.slice(markerIndex + marker.length));
144
+ },
145
+ description: text => theme.fg("muted", text),
146
+ scrollInfo: text => theme.fg("muted", text),
147
+ noMatch: text => theme.fg("muted", text),
109
148
  }, THINKING_SELECT_LIST_LAYOUT);
110
149
  const currentIndex = themedItems.findIndex((item) => item.value === preselect);
111
150
  if (currentIndex !== -1)
112
151
  list.setSelectedIndex(currentIndex);
113
152
  list.onSelect = (item) => this.onSelect(item.value);
114
- return list;
153
+ return { list, presentation: new ThinkingSelectListPresentation(list, themedItems) };
115
154
  }
116
155
  applyFilter(query) {
117
156
  const filtered = query
@@ -120,8 +159,8 @@ export class OwnedThinkingSelectorComponent extends Container {
120
159
  const selectedValue = this.selectList.getSelectedItem()?.value;
121
160
  const newList = this.buildSelectList(filtered, selectedValue);
122
161
  this.selectListContainer.clear();
123
- this.selectListContainer.addChild(newList);
124
- this.selectList = newList;
162
+ this.selectListContainer.addChild(newList.presentation);
163
+ this.selectList = newList.list;
125
164
  }
126
165
  handleInput(keyData) {
127
166
  const kb = getKeybindings();
@@ -134,7 +173,7 @@ export class OwnedThinkingSelectorComponent extends Container {
134
173
  }
135
174
  return;
136
175
  }
137
- if (matchesKey(keyData, Key.escape)) {
176
+ if (kb.matches(keyData, "tui.select.cancel")) {
138
177
  this.onCancel();
139
178
  return;
140
179
  }