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

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 (62) 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 +71 -15
  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/settings-route-host.js +1 -1
  8. package/dist/contracts/presentation/index.d.ts +5 -0
  9. package/dist/contracts/presentation/index.js +2 -0
  10. package/dist/features/owned-ui/project-trust-dialog.js +7 -5
  11. package/dist/features/owned-ui/reference-screen-app.d.ts +2 -2
  12. package/dist/features/owned-ui/reference-screen-app.js +10 -6
  13. package/dist/features/owned-ui/settings-app.js +10 -5
  14. package/dist/integrations/pi/components/index.d.ts +3 -2
  15. package/dist/integrations/pi/components/index.js +2 -1
  16. package/dist/integrations/pi/components/modal-frame.d.ts +11 -1
  17. package/dist/integrations/pi/components/modal-frame.js +29 -1
  18. package/dist/integrations/pi/components/models-dialog.js +17 -17
  19. package/dist/integrations/pi/components/share-operation-dialog.d.ts +6 -0
  20. package/dist/integrations/pi/components/share-operation-dialog.js +40 -0
  21. package/dist/integrations/pi/components/shell-editor-autocomplete.js +15 -6
  22. package/dist/integrations/pi/components/shell-extension-ui.d.ts +3 -0
  23. package/dist/integrations/pi/components/shell-extension-ui.js +8 -4
  24. package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +0 -4
  25. package/dist/integrations/pi/components/shell-selectors-dialogs.js +56 -34
  26. package/dist/integrations/pi/components/shell-session-info-reference.js +35 -2
  27. package/dist/integrations/pi/components/shell-shared-facade.d.ts +3 -1
  28. package/dist/integrations/pi/components/shell-shared-facade.js +13 -1
  29. package/dist/integrations/pi/components/skills-dialog.js +19 -6
  30. package/dist/integrations/pi/components/theme.d.ts +8 -1
  31. package/dist/integrations/pi/components/theme.js +28 -1
  32. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +10 -2
  33. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +20 -6
  34. package/dist/integrations/pi/components/upstream/components/extension-editor.js +2 -2
  35. package/dist/integrations/pi/components/upstream/components/extension-input.js +4 -5
  36. package/dist/integrations/pi/components/upstream/components/extension-selector.js +2 -3
  37. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +3 -2
  38. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +4 -15
  39. package/dist/integrations/pi/components/upstream/components/session-selector.d.ts +2 -0
  40. package/dist/integrations/pi/components/upstream/components/session-selector.js +136 -109
  41. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +8 -5
  42. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +63 -26
  43. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +29 -20
  44. package/dist/integrations/pi/components/upstream/components/tree-selector.js +245 -210
  45. package/dist/integrations/pi/components/upstream/components/trust-selector.js +13 -15
  46. package/dist/integrations/pi/engine/adapter.js +21 -2
  47. package/dist/integrations/pi/engine/workflow-runner.js +2 -1
  48. package/dist/native/darwin-arm64/manifest.json +1 -1
  49. package/dist/native/linux-x64/manifest.json +1 -1
  50. package/dist/native/win32-x64/manifest.json +2 -2
  51. package/dist/native/win32-x64/process-guardian.exe +0 -0
  52. package/dist/ui/apps/host.d.ts +4 -3
  53. package/dist/ui/apps/host.js +9 -4
  54. package/dist/ui/components/dialog-panel.js +6 -7
  55. package/dist/ui/components/index.d.ts +1 -1
  56. package/dist/ui/components/index.js +1 -1
  57. package/dist/ui/components/list-view.d.ts +3 -3
  58. package/dist/ui/components/list-view.js +8 -9
  59. package/dist/ui/components/shortcut-hints.d.ts +2 -0
  60. package/dist/ui/components/shortcut-hints.js +17 -1
  61. package/dist/ui/components/theme.d.ts +1 -1
  62. package/package.json +1 -1
@@ -10,7 +10,7 @@
10
10
  import { DynamicBorder } from "../../../startup-public.js";
11
11
  import { Container, getKeybindings, Input, Spacer, Text } from "@earendil-works/pi-tui";
12
12
  import { addPiModalHeader, adoptPiModalFrame } from "../../modal-frame.js";
13
- import { piTheme, renderPiModalShortcutHints } from "../../theme.js";
13
+ import { DIALOG_CLOSE_SHORTCUT_HINT, piTheme, renderPiModalShortcutHints } from "../../theme.js";
14
14
  import { CountdownTimer } from "./countdown-timer.js";
15
15
  /** Bare-A1 extension text input using the common modal shortcut row. */
16
16
  export class ExtensionInputComponent extends Container {
@@ -32,7 +32,7 @@ export class ExtensionInputComponent extends Container {
32
32
  this.#onCancel = onCancel;
33
33
  this.#baseTitle = title;
34
34
  const theme = piTheme();
35
- this.#titleText = new Text(theme.fg("accent", title), 0, 0);
35
+ this.#titleText = new Text(theme.fg("accent", theme.bold(title)), 0, 0);
36
36
  const header = addPiModalHeader(this, new DynamicBorder(), this.#titleText);
37
37
  if (opts?.description) {
38
38
  this.addChild(new Spacer(1));
@@ -42,7 +42,7 @@ export class ExtensionInputComponent extends Container {
42
42
  this.#countdown = opts?.timeout !== undefined && opts.timeout > 0 && opts.tui !== undefined
43
43
  ? new CountdownTimer(opts.timeout, opts.tui, seconds => {
44
44
  const current = piTheme();
45
- this.#titleText.setText(current.fg("accent", `${this.#baseTitle} (${seconds}s)`));
45
+ this.#titleText.setText(current.fg("accent", current.bold(`${this.#baseTitle} (${seconds}s)`)));
46
46
  }, () => this.#onCancel())
47
47
  : undefined;
48
48
  if (opts?.initialValue !== undefined)
@@ -52,9 +52,8 @@ export class ExtensionInputComponent extends Container {
52
52
  const keys = getKeybindings();
53
53
  this.addChild(new Text(renderPiModalShortcutHints([
54
54
  { key: keys.getKeys("tui.select.confirm").join("/"), action: "submit" },
55
- { key: keys.getKeys("tui.select.cancel").join("/"), action: "cancel" },
55
+ DIALOG_CLOSE_SHORTCUT_HINT,
56
56
  ]), 0, 0));
57
- this.addChild(new Spacer(1));
58
57
  this.addChild(new DynamicBorder());
59
58
  adoptPiModalFrame(this, { topIndex: 0, bottomIndex: this.children.length - 1, header });
60
59
  }
@@ -10,7 +10,7 @@
10
10
  import { DynamicBorder } from "../../../startup-public.js";
11
11
  import { Container, getKeybindings, Spacer, Text } from "@earendil-works/pi-tui";
12
12
  import { addPiModalHeader, adoptPiModalFrame } from "../../modal-frame.js";
13
- import { piTheme, renderPiModalShortcutHints } from "../../theme.js";
13
+ import { DIALOG_CLOSE_SHORTCUT_HINT, piTheme, renderPiModalShortcutHints } from "../../theme.js";
14
14
  import { CountdownTimer } from "./countdown-timer.js";
15
15
  /** Bare-A1 extension selector using the common modal shortcut row. */
16
16
  export class ExtensionSelectorComponent extends Container {
@@ -50,9 +50,8 @@ export class ExtensionSelectorComponent extends Container {
50
50
  this.addChild(new Text(renderPiModalShortcutHints([
51
51
  { key: "↑↓", action: "navigate" },
52
52
  { key: keys.getKeys("tui.select.confirm").join("/"), action: "select" },
53
- { key: keys.getKeys("tui.select.cancel").join("/"), action: "cancel" },
53
+ DIALOG_CLOSE_SHORTCUT_HINT,
54
54
  ]), 0, 0));
55
- this.addChild(new Spacer(1));
56
55
  this.addChild(new DynamicBorder());
57
56
  adoptPiModalFrame(this, { topIndex: 0, bottomIndex: this.children.length - 1, header });
58
57
  this.#updateList();
@@ -5,8 +5,9 @@
5
5
  * search, bulk/provider/reorder actions, dirty state, Ctrl+S persistence, refresh status,
6
6
  * cancellation, and focus while remapping theme and public helper imports; local key labels preserve
7
7
  * pinned platform formatting before layout without changing binding identities, and bare A1 uses the
8
- * shared semantic modal shortcut row and compact padded modal frame.
9
- * Deviations: owned-modal-shortcut-hints.
8
+ * shared semantic modal shortcut row and compact padded modal frame while immediately closing on the
9
+ * implicit Ctrl+C selection-cancel alias without advertising it.
10
+ * Deviations: owned-modal-shortcut-hints, owned-dialog-ctrl-c-cancel.
10
11
  */
11
12
  interface ScopedModel {
12
13
  readonly provider: string;
@@ -1,8 +1,8 @@
1
- import { Container, fuzzyFilter, getKeybindings, Input, Key, matchesKey, Spacer, Text, } from "@earendil-works/pi-tui";
1
+ import { Container, fuzzyFilter, getKeybindings, Input, Spacer, Text, } from "@earendil-works/pi-tui";
2
2
  import { DynamicBorder } from "../../../startup-public.js";
3
3
  import { getModelSearchText } from "../model-search.js";
4
4
  import { addPiModalHeader, adoptPiModalFrame } from "../../modal-frame.js";
5
- import { piTheme, renderPiModalShortcutHints } from "../../theme.js";
5
+ import { DIALOG_CLOSE_SHORTCUT_HINT, piTheme, renderPiModalShortcutHints } from "../../theme.js";
6
6
  // Rationale: upstream's capitalized hint formatter is private to the package; the same mapping lives here.
7
7
  function keyDisplayText(keybinding) {
8
8
  return getKeybindings().getKeys(keybinding)
@@ -179,6 +179,7 @@ export class ScopedModelsSelectorComponent extends Container {
179
179
  { key: reorderKeys, action: "reorder" },
180
180
  { key: keyDisplayText("app.models.save"), action: "save" },
181
181
  { action: countText },
182
+ DIALOG_CLOSE_SHORTCUT_HINT,
182
183
  ]);
183
184
  return this.isDirty ? `${hint} ${theme.fg("warning", "(unsaved)")}` : hint;
184
185
  }
@@ -321,19 +322,7 @@ export class ScopedModelsSelectorComponent extends Container {
321
322
  this.footerText.setText(this.getFooterText());
322
323
  return;
323
324
  }
324
- // Ctrl+C - clear search or cancel if empty
325
- if (matchesKey(data, Key.ctrl("c"))) {
326
- if (this.searchInput.getValue()) {
327
- this.searchInput.setValue("");
328
- this.refresh();
329
- }
330
- else {
331
- this.callbacks.onCancel();
332
- }
333
- return;
334
- }
335
- // Escape - cancel
336
- if (matchesKey(data, Key.escape)) {
325
+ if (kb.matches(data, "tui.select.cancel")) {
337
326
  this.callbacks.onCancel();
338
327
  return;
339
328
  }
@@ -45,6 +45,7 @@ declare class SessionList implements Component, Focusable {
45
45
  private setConfirmingDeletePath;
46
46
  private startDeleteConfirmationForSelectedSession;
47
47
  private isCurrentSessionPath;
48
+ private sessionPathText;
48
49
  invalidate(): void;
49
50
  render(width: number): string[];
50
51
  private buildTreePrefix;
@@ -59,6 +60,7 @@ export declare class SessionSelectorComponent extends Container implements Focus
59
60
  private canRename;
60
61
  private sessionList;
61
62
  private header;
63
+ private footer;
62
64
  private keybindings;
63
65
  private scope;
64
66
  private sortMode;
@@ -2,20 +2,25 @@
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, and bottom dynamic feedback and
10
+ * shortcut footer. Selected session rows use the blue selection surface, success-green primary title,
11
+ * muted metadata, and ordinary text weight.
12
+ * Deviations: owned-modal-shortcut-hints, owned-resume-session-dialog,
13
+ * owned-standard-dialog-selection.
9
14
  */
10
15
  import { spawnSync } from "node:child_process";
11
16
  import { existsSync, realpathSync } from "node:fs";
12
17
  import { unlink } from "node:fs/promises";
13
18
  import * as os from "node:os";
14
- import { Container, getKeybindings, Input, Spacer, Text, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
19
+ import { Container, getKeybindings, Input, matchesKey, sliceByColumn, Spacer, Text, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
15
20
  import { KeybindingsManager } from "../adjacent/core/keybindings.js";
16
21
  import { DynamicBorder } from "../../../startup-public.js";
17
22
  import { addPiModalHeader, adoptPiModalFrame } from "../../modal-frame.js";
18
- import { piTheme, renderPiModalShortcutHints } from "../../theme.js";
23
+ import { DIALOG_CLOSE_SHORTCUT_HINT, piTheme, renderPiModalShortcutHints } from "../../theme.js";
19
24
  const theme = new Proxy({}, {
20
25
  get(_target, property) {
21
26
  const active = piTheme();
@@ -29,6 +34,16 @@ function keyText(keybinding, keybindings) {
29
34
  function shortcutHint(keybinding, action, keybindings) {
30
35
  return { key: keyText(keybinding, keybindings), action };
31
36
  }
37
+ /** Keep the canonical close control complete while clipping preceding state-specific guidance. */
38
+ function withCloseHint(content, width) {
39
+ const close = renderPiModalShortcutHints([DIALOG_CLOSE_SHORTCUT_HINT]);
40
+ const closeWidth = visibleWidth(close);
41
+ if (width <= closeWidth)
42
+ return truncateToWidth(close, width, "");
43
+ const bodyWidth = Math.max(0, width - closeWidth - 2);
44
+ const body = truncateToWidth(content, bodyWidth, "…");
45
+ return body.length === 0 ? close : `${body} ${close}`;
46
+ }
32
47
  import { filterAndSortSessions, hasSessionName } from "./session-selector-search.js";
33
48
  function shortenPath(path) {
34
49
  const home = os.homedir();
@@ -59,6 +74,27 @@ function formatSessionDate(date) {
59
74
  return `${Math.floor(diffDays / 30)}mo`;
60
75
  return `${Math.floor(diffDays / 365)}y`;
61
76
  }
77
+ function fitToWidth(value, width, ellipsis = "") {
78
+ const available = Math.max(0, width);
79
+ const clipped = truncateToWidth(value, available, ellipsis);
80
+ return `${clipped}${" ".repeat(Math.max(0, available - visibleWidth(clipped)))}`;
81
+ }
82
+ function fitPathToWidth(value, width, preserveTail) {
83
+ if (!preserveTail || visibleWidth(value) <= width)
84
+ return fitToWidth(value, width, "…");
85
+ const tailWidth = Math.max(0, width - 1);
86
+ const tail = sliceByColumn(value, Math.max(0, visibleWidth(value) - tailWidth), tailWidth, true);
87
+ return fitToWidth(`…${tail}`, width);
88
+ }
89
+ function renderSelectedRow(value) {
90
+ const marker = "\u0000";
91
+ const wrapper = theme.bg("selectedBg", marker);
92
+ const markerIndex = wrapper.indexOf(marker);
93
+ const on = wrapper.slice(0, markerIndex);
94
+ const off = wrapper.slice(markerIndex + marker.length);
95
+ const reasserted = value.replace(/\u001b\[[0-?]*[ -/]*m/gu, sequence => `${sequence}${on}`);
96
+ return `${on}${reasserted}${off}`;
97
+ }
62
98
  function canonicalizePath(path) {
63
99
  if (!path)
64
100
  return path;
@@ -75,8 +111,6 @@ class SessionSelectorHeader {
75
111
  nameFilter;
76
112
  requestRender;
77
113
  keybindings;
78
- loading = false;
79
- loadProgress = null;
80
114
  showPath = false;
81
115
  confirmingDeletePath = null;
82
116
  statusMessage = null;
@@ -98,14 +132,6 @@ class SessionSelectorHeader {
98
132
  setNameFilter(nameFilter) {
99
133
  this.nameFilter = nameFilter;
100
134
  }
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
135
  setShowPath(showPath) {
110
136
  this.showPath = showPath;
111
137
  }
@@ -134,64 +160,58 @@ class SessionSelectorHeader {
134
160
  }
135
161
  invalidate() { }
136
162
  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";
163
+ const title = theme.fg("accent", theme.bold("Resume Session"));
164
+ const scopeText = theme.fg("muted", "Filter: ")
165
+ + theme.fg(this.scope === "current" ? "accent" : "dim", "current")
166
+ + theme.fg("muted", " | ")
167
+ + theme.fg(this.scope === "all" ? "accent" : "dim", "all");
168
+ const nameText = theme.fg("muted", "Name: ") + theme.fg("accent", this.nameFilter);
169
+ const sortLabel = this.sortMode === "relevance" ? "fuzzy" : this.sortMode;
140
170
  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;
171
+ const status = `${scopeText} ${nameText} ${sortText}`;
172
+ return [truncateToWidth(title, width, ""), truncateToWidth(status, width, "")];
173
+ }
174
+ renderFooter(width) {
161
175
  if (this.confirmingDeletePath !== null) {
162
176
  const confirmHint = theme.fg("error", "Delete session? ") + renderPiModalShortcutHints([
163
177
  shortcutHint("tui.select.confirm", "confirm", this.keybindings),
164
- shortcutHint("tui.select.cancel", "cancel", this.keybindings),
165
178
  ]);
166
- hintLine1 = truncateToWidth(confirmHint, width, "…");
167
- hintLine2 = "";
179
+ return [withCloseHint(confirmHint, width)];
168
180
  }
169
- else if (this.statusMessage) {
181
+ if (this.statusMessage) {
170
182
  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];
183
+ return [withCloseHint(theme.fg(color, this.statusMessage.message), width)];
184
+ }
185
+ const pathState = this.showPath ? "(on)" : "(off)";
186
+ const hint1 = renderPiModalShortcutHints([
187
+ shortcutHint("tui.input.tab", "scope", this.keybindings),
188
+ { action: "re:<pattern> regex" },
189
+ { action: '"phrase" exact' },
190
+ ]);
191
+ const hint2Parts = [
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
+ hint2Parts.push(shortcutHint("app.session.rename", "rename", this.keybindings));
199
+ return [
200
+ truncateToWidth(hint1, width, "…"),
201
+ withCloseHint(renderPiModalShortcutHints(hint2Parts), width),
202
+ ];
203
+ }
204
+ }
205
+ class SessionSelectorFooter {
206
+ presentation;
207
+ constructor(presentation) {
208
+ this.presentation = presentation;
209
+ }
210
+ invalidate() {
211
+ this.presentation.invalidate();
212
+ }
213
+ render(width) {
214
+ return this.presentation.renderFooter(width);
195
215
  }
196
216
  }
197
217
  /**
@@ -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,15 @@
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 without advertising that alias. All list and border colors use the owned theme
12
+ * and its explicit color mode. Selected rows use the shared bare-A1 blue selection surface only around
13
+ * the rendered item span, with the existing accent arrow, normal-text level, and preserved
14
+ * current/default/description roles. The comparison profile retains the public pinned component.
12
15
  * Deviations: owned-modal-shortcut-hints, owned-level-cycle-shortcut, owned-thinking-selector-heading,
13
- * owned-thinking-selector-controls.
16
+ * owned-thinking-selector-controls, owned-dialog-ctrl-c-cancel, owned-standard-dialog-selection.
14
17
  */
15
18
  import { Container, type Focusable, SelectList } from "@earendil-works/pi-tui";
16
19
  export type ThinkingSelectorLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";