@timurproko/a1 0.2.5-dev.698 → 0.2.5

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 (38) hide show
  1. package/dist/app/session-shell/session-shell.js +9 -8
  2. package/dist/composition/owned-ui.js +2 -0
  3. package/dist/contracts/presentation/index.d.ts +6 -1
  4. package/dist/contracts/presentation/index.js +4 -2
  5. package/dist/features/owned-ui/resources/release-notes.json +4 -0
  6. package/dist/features/owned-ui/settings-app.d.ts +1 -1
  7. package/dist/features/owned-ui/settings-app.js +334 -63
  8. package/dist/integrations/pi/components/models-dialog.js +2 -2
  9. package/dist/integrations/pi/components/shell-editor-autocomplete.js +40 -2
  10. package/dist/integrations/pi/components/skills-dialog.js +2 -2
  11. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +4 -2
  12. package/dist/integrations/pi/components/upstream/components/owned-editor.js +48 -12
  13. package/dist/integrations/pi/components/upstream/components/session-selector.js +14 -14
  14. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +5 -4
  15. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +6 -4
  16. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +7 -6
  17. package/dist/integrations/pi/components/upstream/components/tree-selector.js +24 -18
  18. package/dist/native/darwin-arm64/manifest.json +1 -1
  19. package/dist/native/linux-x64/manifest.json +1 -1
  20. package/dist/native/win32-x64/manifest.json +2 -2
  21. package/dist/native/win32-x64/process-guardian.exe +0 -0
  22. package/dist/ui/components/dialog-panel.d.ts +15 -0
  23. package/dist/ui/components/dialog-panel.js +50 -3
  24. package/dist/ui/components/index.d.ts +3 -3
  25. package/dist/ui/components/index.js +2 -2
  26. package/dist/ui/components/line-input.d.ts +2 -0
  27. package/dist/ui/components/line-input.js +2 -2
  28. package/dist/ui/components/prompt-input.d.ts +2 -2
  29. package/dist/ui/components/prompt-input.js +5 -5
  30. package/dist/ui/components/shortcut-hints.d.ts +8 -3
  31. package/dist/ui/components/shortcut-hints.js +25 -5
  32. package/dist/ui/components/shortcuts.d.ts +1 -1
  33. package/dist/ui/components/shortcuts.js +4 -0
  34. package/dist/ui/components/value-menu.js +3 -1
  35. package/dist/ui/settings/manager.d.ts +1 -0
  36. package/dist/ui/settings/manager.js +9 -3
  37. package/docs/releases/0.2.5.md +35 -0
  38. package/package.json +1 -1
@@ -161,8 +161,12 @@ export function createPiShellEditor(options) {
161
161
  const resources = commands
162
162
  .filter(command => command.name !== collapsed.collapsedCommand?.name && command.source !== "builtin" && !builtInNames.has(command.name))
163
163
  .map(command => autocompleteCommand(command));
164
- const combined = new CombinedAutocompleteProvider([...builtIns, ...resources], options.cwd ?? process.cwd());
165
- autocompleteProvider = tunnelSkills.length === 0 ? combined : createSkillsTunnelProvider(combined, tunnelSkills);
164
+ const catalog = [...builtIns, ...resources];
165
+ const combined = new CombinedAutocompleteProvider(catalog, options.cwd ?? process.cwd());
166
+ const tunnelAware = tunnelSkills.length === 0 ? combined : createSkillsTunnelProvider(combined, tunnelSkills);
167
+ autocompleteProvider = options.keybindingProfile === "a1"
168
+ ? createDelimiterReadyCommandProvider(tunnelAware, catalog)
169
+ : tunnelAware;
166
170
  editor.setAutocompleteProvider(autocompleteProvider);
167
171
  };
168
172
  setAutocompleteCommands(options.autocompleteCommands ?? []);
@@ -345,6 +349,40 @@ export function createPiShellEditor(options) {
345
349
  cancelPendingPastes: () => editorUx?.cancelPendingPastes(),
346
350
  };
347
351
  }
352
+ /** Bare A1 leaves an applied catalog command ready for `:`, a user-entered space, or submission. */
353
+ function createDelimiterReadyCommandProvider(base, commands) {
354
+ const commandNames = new Set(commands.map(command => command.name));
355
+ return {
356
+ ...(base.triggerCharacters === undefined ? {} : { triggerCharacters: base.triggerCharacters }),
357
+ getSuggestions: (lines, cursorLine, cursorCol, options) => base.getSuggestions(lines, cursorLine, cursorCol, options),
358
+ applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
359
+ const result = base.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
360
+ const currentLine = lines[cursorLine] ?? "";
361
+ const textBeforeCursor = currentLine.slice(0, cursorCol);
362
+ const beforePrefix = currentLine.slice(0, cursorCol - prefix.length);
363
+ const completedLine = result.lines[result.cursorLine] ?? "";
364
+ const spacerIndex = result.cursorCol - 1;
365
+ const expectedCommand = `${beforePrefix}/${item.value}`;
366
+ const isCatalogCommand = commandNames.has(item.value)
367
+ && prefix.startsWith("/")
368
+ && !prefix.slice(1).includes("/")
369
+ && !prefix.includes(" ")
370
+ && textBeforeCursor.trimStart() === prefix;
371
+ if (!isCatalogCommand
372
+ || result.cursorLine !== cursorLine
373
+ || spacerIndex < 0
374
+ || completedLine[spacerIndex] !== " "
375
+ || completedLine.slice(0, spacerIndex) !== expectedCommand)
376
+ return result;
377
+ const nextLines = [...result.lines];
378
+ nextLines[result.cursorLine] = completedLine.slice(0, spacerIndex) + completedLine.slice(spacerIndex + 1);
379
+ return { ...result, lines: nextLines, cursorCol: spacerIndex };
380
+ },
381
+ ...(base.shouldTriggerFileCompletion === undefined ? {} : {
382
+ shouldTriggerFileCompletion: (lines, cursorLine, cursorCol) => base.shouldTriggerFileCompletion(lines, cursorLine, cursorCol),
383
+ }),
384
+ };
385
+ }
348
386
  function autocompleteCommand(command, addition) {
349
387
  const argumentOptions = addition?.argumentOptions ?? command.argumentOptions;
350
388
  return {
@@ -27,8 +27,7 @@ class SkillRow {
27
27
  }
28
28
  }
29
29
  /**
30
- * The A1-owned searchable Skills dialog. Its composition is the model selector's (border, spacer,
31
- * search input, spacer, list, spacer, hint footer, border) built from public pi-tui components; its
30
+ * The A1-owned searchable Skills dialog uses the model selector's public pi-tui composition; its
32
31
  * content follows the v2 skills extension: an accent bold title, bare skill-name rows, the selected
33
32
  * skill's description cut to one line under the rows, the pinned scroll counter on overflow,
34
33
  * and the two empty states. Enter applies the selected skill with no arguments; the query is never appended.
@@ -57,6 +56,7 @@ class SkillsSelectorComponent extends Container {
57
56
  this.addChild(new Spacer(1));
58
57
  const bindings = getKeybindings();
59
58
  this.addChild(new Text(renderPiModalShortcutHints([
59
+ { key: "Type", action: "search" },
60
60
  { key: "↑↓", action: "navigate" },
61
61
  { key: bindings.getKeys("tui.select.confirm").join("/"), action: "select" },
62
62
  DIALOG_CLOSE_SHORTCUT_HINT,
@@ -5,9 +5,11 @@
5
5
  * private upstream keybinding constructor dependency; bare A1 injects the shared Settings/agent input
6
6
  * frame, transient contextual-suggestion branch, and explicit body geometry for selection and
7
7
  * above-prompt autocomplete. Bare A1 also clears a sole top-level slash-command search on Escape. Bare
8
- * A1 also completes a selected tunnel command with `:` and reopens the menu on its tunnel rows.
8
+ * A1 also completes a selected or exact completed tunnel command with `:` and reopens the menu on its
9
+ * tunnel rows, and synchronously retains then refreshes an exact top-level command menu after Tab
10
+ * applies its row.
9
11
  * Deviations: owned-shared-input-frame, above-prompt-autocomplete-placement,
10
- * clear-command-search-on-escape, command-tunnel-colon-completion.
12
+ * clear-command-search-on-escape, command-tunnel-colon-completion, keep-command-menu-open-after-tab.
11
13
  */
12
14
  import { type EditorOptions, type EditorTheme, type TUI } from "@earendil-works/pi-tui";
13
15
  import type { AppKeybinding, KeybindingsManager } from "../adjacent/core/keybindings.js";
@@ -5,9 +5,11 @@
5
5
  * private upstream keybinding constructor dependency; bare A1 injects the shared Settings/agent input
6
6
  * frame, transient contextual-suggestion branch, and explicit body geometry for selection and
7
7
  * above-prompt autocomplete. Bare A1 also clears a sole top-level slash-command search on Escape. Bare
8
- * A1 also completes a selected tunnel command with `:` and reopens the menu on its tunnel rows.
8
+ * A1 also completes a selected or exact completed tunnel command with `:` and reopens the menu on its
9
+ * tunnel rows, and synchronously retains then refreshes an exact top-level command menu after Tab
10
+ * applies its row.
9
11
  * Deviations: owned-shared-input-frame, above-prompt-autocomplete-placement,
10
- * clear-command-search-on-escape, command-tunnel-colon-completion.
12
+ * clear-command-search-on-escape, command-tunnel-colon-completion, keep-command-menu-open-after-tab.
11
13
  */
12
14
  import { CURSOR_MARKER, Editor, visibleWidth, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
13
15
  const PROMPT_GRAPHEMES = new Intl.Segmenter(undefined, { granularity: "grapheme" });
@@ -78,7 +80,10 @@ export function createOwnedEditorClass(Base) {
78
80
  handleInput(data) {
79
81
  if (this.onExtensionShortcut?.(data))
80
82
  return;
81
- if (data === ":" && this.#completeSelectedCommandTunnel())
83
+ const tabbedCommand = this.keybindings.matches(data, "tui.input.tab") && this.isTopLevelCommandSearch()
84
+ ? retainedAutocomplete(this)
85
+ : undefined;
86
+ if (data === ":" && this.#completeCommandTunnel())
82
87
  return;
83
88
  if (this.#promptSuggestion !== null
84
89
  && !this.isShowingAutocomplete()
@@ -133,19 +138,41 @@ export function createOwnedEditorClass(Base) {
133
138
  }
134
139
  }
135
140
  super.handleInput(data);
141
+ if (tabbedCommand !== undefined)
142
+ this.#retainCompletedCommandMenu(tabbedCommand);
143
+ }
144
+ /** Keep the current menu visible while refreshing it for the exact completed command. */
145
+ #retainCompletedCommandMenu(completion) {
146
+ const text = this.getText();
147
+ const cursor = this.getCursor();
148
+ if (text !== `/${completion.value}` || cursor.line !== 0 || cursor.col !== text.length)
149
+ return;
150
+ Reflect.set(this, "autocompleteList", completion.list);
151
+ Reflect.set(this, "autocompleteState", completion.state);
152
+ Reflect.set(this, "autocompletePrefix", text);
153
+ triggerAutocomplete(this);
136
154
  }
137
155
  /**
138
- * Replace a sole top-level slash search whose selected row is a tunnel command with `/<command>:`
139
- * and reopen the menu on the tunnel rows. The replacement goes through the public setText, which
140
- * records the undo snapshot; every other colon stays ordinary text.
156
+ * Complete either a selected tunnel row or an exact command left by Tab to `/<command>:` and
157
+ * reopen the menu on the tunnel rows. Public setText records the undo snapshot; every other colon
158
+ * stays ordinary text.
141
159
  */
142
- #completeSelectedCommandTunnel() {
143
- if (!this.isTopLevelCommandSearch())
144
- return false;
145
- const selected = selectedAutocompleteValue(this);
146
- if (selected === undefined || !this.#commandTunnels().includes(selected))
160
+ #completeCommandTunnel() {
161
+ const tunnels = this.#commandTunnels();
162
+ let command;
163
+ if (this.isTopLevelCommandSearch()) {
164
+ const selected = selectedAutocompleteValue(this);
165
+ if (selected !== undefined && tunnels.includes(selected))
166
+ command = selected;
167
+ }
168
+ else if (!this.isShowingAutocomplete()) {
169
+ const text = this.getText();
170
+ const cursor = this.getCursor();
171
+ command = tunnels.find(candidate => text === `/${candidate}` && cursor.line === 0 && cursor.col === text.length);
172
+ }
173
+ if (command === undefined)
147
174
  return false;
148
- this.setText(`/${selected}:`);
175
+ this.setText(`/${command}:`);
149
176
  triggerAutocomplete(this);
150
177
  return true;
151
178
  }
@@ -200,6 +227,15 @@ export function createOwnedEditorClass(Base) {
200
227
  }
201
228
  export class OwnedEditor extends createOwnedEditorClass(Editor) {
202
229
  }
230
+ /** Capture the active selected command list so exact Tab application can retain it synchronously. */
231
+ function retainedAutocomplete(editor) {
232
+ const value = selectedAutocompleteValue(editor);
233
+ const list = Reflect.get(editor, "autocompleteList");
234
+ const state = Reflect.get(editor, "autocompleteState");
235
+ if (value === undefined || typeof list !== "object" || list === null || (state !== "regular" && state !== "force"))
236
+ return undefined;
237
+ return { value, list, state };
238
+ }
203
239
  /** The value of the row the open autocomplete list highlights, read from either editor's list. */
204
240
  function selectedAutocompleteValue(editor) {
205
241
  const list = Reflect.get(editor, "autocompleteList");
@@ -6,9 +6,10 @@
6
6
  * progressive partial results, cancellation, focus, and disposal while remapping public helpers, owned
7
7
  * keybindings/theme, canonical path handling, reverse-Tab scope cycling, and the shared bare-A1 modal
8
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.
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.
12
13
  * Deviations: owned-modal-shortcut-hints, owned-resume-session-dialog,
13
14
  * owned-standard-dialog-selection.
14
15
  */
@@ -183,23 +184,19 @@ class SessionSelectorHeader {
183
184
  return [withCloseHint(theme.fg(color, this.statusMessage.message), width)];
184
185
  }
185
186
  const pathState = this.showPath ? "(on)" : "(off)";
186
- const hint1 = renderPiModalShortcutHints([
187
+ const hints = [
188
+ { key: "Type", action: "search" },
189
+ { key: "↑↓", action: "navigate" },
190
+ shortcutHint("tui.select.confirm", "select", this.keybindings),
187
191
  shortcutHint("tui.input.tab", "scope", this.keybindings),
188
- { action: "re:<pattern> regex" },
189
- { action: '"phrase" exact' },
190
- ]);
191
- const hint2Parts = [
192
192
  shortcutHint("app.session.toggleSort", "sort", this.keybindings),
193
193
  shortcutHint("app.session.toggleNamedFilter", "named", this.keybindings),
194
194
  shortcutHint("app.session.delete", "delete", this.keybindings),
195
195
  shortcutHint("app.session.togglePath", `path ${pathState}`, this.keybindings),
196
196
  ];
197
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
- ];
198
+ hints.push(shortcutHint("app.session.rename", "rename", this.keybindings));
199
+ return [withCloseHint(renderPiModalShortcutHints(hints), width)];
203
200
  }
204
201
  }
205
202
  class SessionSelectorFooter {
@@ -320,7 +317,10 @@ class SessionList {
320
317
  constructor(sessions, showCwd, sortMode, nameFilter, keybindings, currentSessionFilePath) {
321
318
  this.allSessions = sessions;
322
319
  this.filteredSessions = [];
323
- 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
+ });
324
324
  this.showCwd = showCwd;
325
325
  this.sortMode = sortMode;
326
326
  this.nameFilter = nameFilter;
@@ -8,10 +8,11 @@
8
8
  * levels, and rendering exclusive radio-style default markers before level names, an item-adjacent
9
9
  * active checkmark, and aligned muted descriptions. Bare A1 persists defaults immediately on Space,
10
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.
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.
15
16
  * Deviations: owned-modal-shortcut-hints, owned-level-cycle-shortcut, owned-thinking-selector-heading,
16
17
  * owned-thinking-selector-controls, owned-dialog-ctrl-c-cancel, owned-standard-dialog-selection.
17
18
  */
@@ -8,10 +8,11 @@
8
8
  * levels, and rendering exclusive radio-style default markers before level names, an item-adjacent
9
9
  * active checkmark, and aligned muted descriptions. Bare A1 persists defaults immediately on Space,
10
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.
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.
15
16
  * Deviations: owned-modal-shortcut-hints, owned-level-cycle-shortcut, owned-thinking-selector-heading,
16
17
  * owned-thinking-selector-controls, owned-dialog-ctrl-c-cancel, owned-standard-dialog-selection.
17
18
  */
@@ -96,6 +97,7 @@ export class OwnedThinkingSelectorComponent extends Container {
96
97
  this.addChild(this.selectListContainer);
97
98
  this.addChild(new Spacer(1));
98
99
  this.addChild(new Text(renderPiModalShortcutHints([
100
+ { key: "Type", action: "search" },
99
101
  { key: this.keyDisplayText("tui.select.confirm"), action: "select" },
100
102
  { key: "Space", action: "default" },
101
103
  DIALOG_CLOSE_SHORTCUT_HINT,
@@ -6,11 +6,12 @@
6
6
  * Models-style filter status with Tab/Shift+Tab directional cycling and concise all-first presentation
7
7
  * excluding internal bookkeeping, standard search input, blue item-bounded menu-arrow selection that
8
8
  * preserves per-entry semantic foregrounds and has no path bullets, accent entry labels, bracketed
9
- * timestamps, and plain label-time status, semantic role colors with session naming for the system
10
- * root, standard paging/first-last/non-root containing-branch folding keys with a permanently expanded
11
- * system session entry even behind hidden metadata, single-character ellipses on both clipped edges
12
- * with bracket-delimiter preservation and selected-fragment highlighting, and Models-ordered semantic
13
- * shortcut footers without a redundant select hint.
9
+ * timestamps, numeric result counters without duplicate label-time status, and a stateful time on/off
10
+ * shortcut, semantic role colors with session naming for the system root, standard
11
+ * paging/first-last/non-root containing-branch folding keys with a permanently expanded system session
12
+ * entry even behind hidden metadata, single-character ellipses on both clipped edges with
13
+ * bracket-delimiter preservation and selected-fragment highlighting, and Models-ordered semantic
14
+ * Type/search shortcut footers without a redundant select hint.
14
15
  * Deviations: owned-modal-shortcut-hints, owned-session-tree-dialog.
15
16
  */
16
17
  import { type Component, Container, type Focusable } from "@earendil-works/pi-tui";
@@ -57,10 +58,10 @@ declare class TreeList implements Component {
57
58
  invalidate(): void;
58
59
  getSearchQuery(): string;
59
60
  getFilterMode(): FilterMode;
61
+ isLabelTimestampVisible(): boolean;
60
62
  getSelectedNode(): SessionTreeNode | undefined;
61
63
  copySelected(): void;
62
64
  updateNodeLabel(entryId: string, label: string | undefined, labelTimestamp?: string): void;
63
- private getStatusLabels;
64
65
  render(width: number): string[];
65
66
  private getEntryDisplayText;
66
67
  private formatLabelTimestamp;
@@ -6,11 +6,12 @@
6
6
  * Models-style filter status with Tab/Shift+Tab directional cycling and concise all-first presentation
7
7
  * excluding internal bookkeeping, standard search input, blue item-bounded menu-arrow selection that
8
8
  * preserves per-entry semantic foregrounds and has no path bullets, accent entry labels, bracketed
9
- * timestamps, and plain label-time status, semantic role colors with session naming for the system
10
- * root, standard paging/first-last/non-root containing-branch folding keys with a permanently expanded
11
- * system session entry even behind hidden metadata, single-character ellipses on both clipped edges
12
- * with bracket-delimiter preservation and selected-fragment highlighting, and Models-ordered semantic
13
- * shortcut footers without a redundant select hint.
9
+ * timestamps, numeric result counters without duplicate label-time status, and a stateful time on/off
10
+ * shortcut, semantic role colors with session naming for the system root, standard
11
+ * paging/first-last/non-root containing-branch folding keys with a permanently expanded system session
12
+ * entry even behind hidden metadata, single-character ellipses on both clipped edges with
13
+ * bracket-delimiter preservation and selected-fragment highlighting, and Models-ordered semantic
14
+ * Type/search shortcut footers without a redundant select hint.
14
15
  * Deviations: owned-modal-shortcut-hints, owned-session-tree-dialog.
15
16
  */
16
17
  import { Container, Input, matchesKey, Spacer, sliceByColumn, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
@@ -528,6 +529,9 @@ class TreeList {
528
529
  getFilterMode() {
529
530
  return this.filterMode;
530
531
  }
532
+ isLabelTimestampVisible() {
533
+ return this.showLabelTimestamps;
534
+ }
531
535
  getSelectedNode() {
532
536
  return this.filteredNodes[this.selectedIndex]?.node;
533
537
  }
@@ -550,16 +554,10 @@ class TreeList {
550
554
  }
551
555
  }
552
556
  }
553
- getStatusLabels() {
554
- return this.showLabelTimestamps ? " label time" : "";
555
- }
556
557
  render(width) {
557
558
  const lines = [];
558
559
  if (this.filteredNodes.length === 0) {
559
560
  lines.push(truncateToWidth(theme.fg("muted", " No entries found"), width));
560
- const status = this.getStatusLabels().trim();
561
- if (status)
562
- lines.push(truncateToWidth(theme.fg("muted", ` ${status}`), width));
563
561
  return lines;
564
562
  }
565
563
  const startIndex = Math.max(0, Math.min(this.selectedIndex - Math.floor(this.maxVisibleLines / 2), this.filteredNodes.length - this.maxVisibleLines));
@@ -632,7 +630,7 @@ class TreeList {
632
630
  renderedRows.push({ gutter, body, anchorCol, bodyWidth: visibleWidth(body), rightClipSuffix, isSelected });
633
631
  }
634
632
  lines.push(...renderHorizontalViewport(renderedRows, width));
635
- lines.push(truncateToWidth(theme.fg("muted", ` (${this.selectedIndex + 1}/${this.filteredNodes.length})${this.getStatusLabels()}`), width));
633
+ lines.push(truncateToWidth(theme.fg("muted", ` (${this.selectedIndex + 1}/${this.filteredNodes.length})`), width));
636
634
  return lines;
637
635
  }
638
636
  getEntryDisplayText(node) {
@@ -1096,12 +1094,19 @@ class TreeFilter {
1096
1094
  }
1097
1095
  /** Component that renders tree help as semantic rows with chunk-aware wrapping */
1098
1096
  class TreeHelp {
1097
+ treeList;
1098
+ constructor(treeList) {
1099
+ this.treeList = treeList;
1100
+ }
1099
1101
  invalidate() { }
1100
1102
  render(width) {
1101
1103
  const items = [
1102
- ...TREE_HELP_ITEMS.map(({ keys, label, labelFirst }) => {
1103
- const key = formatHelpKeys(keys);
1104
- return renderPiModalShortcutHints([key ? { key, action: label, ...(labelFirst === undefined ? {} : { actionFirst: labelFirst }) } : { action: label }]);
1104
+ ...TREE_HELP_ITEMS.map(({ keys, label, labelFirst, displayKey }) => {
1105
+ const key = displayKey ?? formatHelpKeys(keys);
1106
+ const action = keys.includes("app.tree.toggleLabelTimestamp")
1107
+ ? `time (${this.treeList.isLabelTimestampVisible() ? "on" : "off"})`
1108
+ : label;
1109
+ return renderPiModalShortcutHints([key ? { key, action, ...(labelFirst === undefined ? {} : { actionFirst: labelFirst }) } : { action }]);
1105
1110
  }),
1106
1111
  renderPiModalShortcutHints([DIALOG_CLOSE_SHORTCUT_HINT]),
1107
1112
  ];
@@ -1131,7 +1136,7 @@ class TreeHelp {
1131
1136
  }
1132
1137
  }
1133
1138
  const TREE_HELP_ITEMS = [
1134
- { keys: [], label: "type to search" },
1139
+ { keys: [], displayKey: "Type", label: "search" },
1135
1140
  { keys: ["tui.select.up", "tui.select.down"], label: "navigate" },
1136
1141
  { keys: ["app.tree.filter.cycleForward"], label: "filter" },
1137
1142
  { keys: ["tui.select.pageUp", "tui.select.pageDown"], label: "page" },
@@ -1139,7 +1144,7 @@ const TREE_HELP_ITEMS = [
1139
1144
  { keys: ["owned.tree.collapse", "owned.tree.expand"], label: "branch" },
1140
1145
  { keys: ["app.message.copy"], label: "copy" },
1141
1146
  { keys: ["app.tree.editLabel"], label: "label" },
1142
- { keys: ["app.tree.toggleLabelTimestamp"], label: "label time" },
1147
+ { keys: ["app.tree.toggleLabelTimestamp"], label: "time" },
1143
1148
  ];
1144
1149
  function formatHelpKeys(keybindings) {
1145
1150
  const keys = [];
@@ -1235,7 +1240,7 @@ export class TreeSelectorComponent extends Container {
1235
1240
  treeContainer = new Container();
1236
1241
  labelInputContainer = new Container();
1237
1242
  footerContainer = new Container();
1238
- treeHelp = new TreeHelp();
1243
+ treeHelp;
1239
1244
  labelInput = null;
1240
1245
  onLabelChangeCallback;
1241
1246
  onCopy;
@@ -1261,6 +1266,7 @@ export class TreeSelectorComponent extends Container {
1261
1266
  this.treeList.onLabelEdit = (entryId, currentLabel) => this.showLabelInput(entryId, currentLabel);
1262
1267
  this.searchInput = new TreeSearchInput(this.treeList);
1263
1268
  this.treeFilter = new TreeFilter(this.treeList);
1269
+ this.treeHelp = new TreeHelp(this.treeList);
1264
1270
  this.titleText = new Text(theme.fg("accent", theme.bold("Session Tree")), 0, 0);
1265
1271
  this.restoreTreeContent();
1266
1272
  const header = addPiModalHeader(this, new DynamicBorder(), this.titleText);
@@ -5,7 +5,7 @@
5
5
  "platform": "darwin",
6
6
  "architecture": "arm64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-10-07T10:23:29.595Z",
8
+ "builtAt": "2026-10-07T15:54:44.546Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "51b0b2dc1c9ae577f2586c4217066929278726c2c15308d9144eb6f538e118ff",
@@ -5,7 +5,7 @@
5
5
  "platform": "linux",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-10-07T10:23:29.449Z",
8
+ "builtAt": "2026-10-07T15:54:41.381Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "ef62679e83bc1fad3d38f9f21675d9256f08d20e67fd4791571032115ca1b0e9",
@@ -5,10 +5,10 @@
5
5
  "platform": "win32",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-10-07T10:24:08.534Z",
8
+ "builtAt": "2026-10-07T15:55:19.867Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian.exe",
11
- "sha256": "a59b20f3c04cf9d9cadff5ecfe7b35449646e9b51a92e0245d74ecc99e639502",
11
+ "sha256": "0c34b684eab04e6d34b7feb5d1f5668f036e55da0af02b3b23516470bc9a4130",
12
12
  "size": 288256
13
13
  },
14
14
  "provenance": {
@@ -1,3 +1,4 @@
1
+ import { LineInput } from "./line-input.js";
1
2
  import { type ShortcutHintEntry } from "./shortcut-hints.js";
2
3
  import type { UiTheme } from "./theme.js";
3
4
  /**
@@ -7,11 +8,13 @@ import type { UiTheme } from "./theme.js";
7
8
  */
8
9
  export interface DialogRow {
9
10
  readonly label: string;
11
+ readonly labelSuffix?: string;
10
12
  readonly value: string;
11
13
  /** What this part does, shown while it is the one in hand. */
12
14
  readonly description?: string;
13
15
  }
14
16
  export interface DialogPanelState {
17
+ readonly title: string;
15
18
  readonly rows: readonly DialogRow[];
16
19
  /** The row in hand. */
17
20
  readonly index: number;
@@ -26,6 +29,16 @@ export interface DialogPanelFrame {
26
29
  /** Column the values start at, so a label stays a label. */
27
30
  readonly valueColumn: number;
28
31
  }
32
+ export interface SteppedDialogPanelState {
33
+ readonly title: string;
34
+ readonly step: number;
35
+ readonly steps: number;
36
+ readonly description: string;
37
+ readonly input?: LineInput;
38
+ readonly rows: readonly DialogRow[];
39
+ readonly index: number;
40
+ readonly hint: readonly ShortcutHintEntry[];
41
+ }
29
42
  /** The column values start at: past the widest label, capped so one long name cannot push them off. */
30
43
  export declare function dialogValueColumn(rows: readonly DialogRow[]): number;
31
44
  /**
@@ -33,5 +46,7 @@ export declare function dialogValueColumn(rows: readonly DialogRow[]): number;
33
46
  * one line below the panel's top.
34
47
  */
35
48
  export declare function renderDialogPanel(state: DialogPanelState, width: number, theme: UiTheme): readonly string[];
49
+ /** Owned stepped-selector composition for structured settings with nested choices. */
50
+ export declare function renderSteppedDialogPanel(state: SteppedDialogPanelState, width: number, theme: UiTheme): readonly string[];
36
51
  /** The row a pointer report lands on, or null when it is not on one. */
37
52
  export declare function dialogRowAt(frame: DialogPanelFrame, row: number, column: number, valueWidth: number): number | null;
@@ -1,6 +1,8 @@
1
- import { renderShortcutHintsWithClose } from "./shortcut-hints.js";
1
+ import { LineInput, renderInputRow } from "./line-input.js";
2
+ import { renderShortcutHints, renderShortcutHintsWithClose, shortcutHintsText } from "./shortcut-hints.js";
2
3
  import { displayWidth, truncateToWidth } from "./text.js";
3
4
  const LABEL_COLUMN_CAP = 30;
5
+ const STEPPED_LABEL_COLUMN_CAP = 46;
4
6
  /** The column values start at: past the widest label, capped so one long name cannot push them off. */
5
7
  export function dialogValueColumn(rows) {
6
8
  const widest = Math.min(LABEL_COLUMN_CAP, Math.max(0, ...rows.map(row => displayWidth(row.label))));
@@ -35,14 +37,59 @@ export function renderDialogPanel(state, width, theme) {
35
37
  const plainHint = typeof state.hint === "string" ? state.hint : " ".repeat(displayWidth(hint));
36
38
  return [
37
39
  rule,
38
- ...rows,
40
+ contentRow(theme.bold(theme.fg("accent", state.title)), state.title),
41
+ contentRow(theme.fg("dim", description), description),
39
42
  "",
40
- contentRow(theme.fg("dim", ` ${description}`), ` ${description}`),
43
+ ...rows,
41
44
  "",
42
45
  contentRow(hint, plainHint),
43
46
  rule,
44
47
  ];
45
48
  }
49
+ /** Owned stepped-selector composition for structured settings with nested choices. */
50
+ export function renderSteppedDialogPanel(state, width, theme) {
51
+ const rule = theme.fg("border", "─".repeat(Math.max(0, width)));
52
+ const contentPadding = Math.min(1, Math.max(0, width));
53
+ const contentWidth = Math.max(0, width - contentPadding);
54
+ const inset = " ".repeat(contentPadding);
55
+ const contentRow = (painted, raw) => `${inset}${pad(truncateToWidth(painted, contentWidth), contentWidth, raw)}`;
56
+ const labelColumn = Math.min(STEPPED_LABEL_COLUMN_CAP, Math.max(0, ...state.rows.map(row => displayWidth(`${row.label}${row.labelSuffix ?? ""}`))));
57
+ const rows = state.rows.map((row, index) => {
58
+ const selected = index === state.index;
59
+ const completeLabel = `${row.label}${row.labelSuffix ?? ""}`;
60
+ const padding = " ".repeat(Math.max(0, labelColumn - displayWidth(completeLabel)));
61
+ const suffix = row.value.length === 0 ? "" : ` ${row.value}`;
62
+ const cursor = selected ? "→ " : " ";
63
+ const raw = `${cursor}${completeLabel}${padding}${suffix}`;
64
+ const label = `${theme.fg("text", row.label)}${theme.fg("muted", row.labelSuffix ?? "")}${padding}`;
65
+ const content = selected
66
+ ? `${theme.fg("accent", cursor)}${label}${theme.fg("muted", suffix)}`
67
+ : `${cursor}${label}${theme.fg("muted", suffix)}`;
68
+ return contentRow(selected ? theme.highlight(content) : content, raw);
69
+ });
70
+ const input = state.input === undefined
71
+ ? []
72
+ : [...renderInputRow(state.input, width, { ruled: false, promptGlyph: "> ", theme }).lines, ""];
73
+ const step = `(step ${state.step}/${state.steps})`;
74
+ const title = `${state.title} ${step}`;
75
+ const paintedTitle = `${theme.bold(theme.fg("accent", state.title))} ${theme.fg("dim", step)}`;
76
+ return [
77
+ rule,
78
+ contentRow(paintedTitle, title),
79
+ contentRow(theme.fg("dim", state.description), state.description),
80
+ "",
81
+ ...input,
82
+ ...rows,
83
+ "",
84
+ renderSteppedHint(state.hint, width, theme),
85
+ rule,
86
+ ];
87
+ }
88
+ function renderSteppedHint(entries, width, theme) {
89
+ const raw = shortcutHintsText(entries, 1, " · ");
90
+ const painted = renderShortcutHints(entries, theme, 1, theme.fg("dim", " · "));
91
+ return `${truncateToWidth(painted, width)}${" ".repeat(Math.max(0, width - displayWidth(raw)))}`;
92
+ }
46
93
  /** The row a pointer report lands on, or null when it is not on one. */
47
94
  export function dialogRowAt(frame, row, column, valueWidth) {
48
95
  const at = row - frame.firstRow;
@@ -1,5 +1,5 @@
1
- export { dialogRowAt, dialogValueColumn, renderDialogPanel } from "./dialog-panel.js";
2
- export type { DialogPanelFrame, DialogPanelState, DialogRow } from "./dialog-panel.js";
1
+ export { dialogRowAt, dialogValueColumn, renderDialogPanel, renderSteppedDialogPanel } from "./dialog-panel.js";
2
+ export type { DialogPanelFrame, DialogPanelState, DialogRow, SteppedDialogPanelState } from "./dialog-panel.js";
3
3
  export { FrameContractError, assertPaneRect, finalizeFrame, validateFrame } from "./frame.js";
4
4
  export type { PaneRect } from "./frame.js";
5
5
  export { humanizeLabel, humanizeTitle } from "./label.js";
@@ -23,7 +23,7 @@ export { MAX_COPY_ROWS, MAX_COPY_SOURCE_UNITS, selectionCopyLineContent, selecti
23
23
  export type { SelectionCopyPrompt, SelectionCopyRow, SelectionCopySnapshot } from "./selection-copy.js";
24
24
  export { ScrollbarRails, isThumbRow, scrollForThumbRow, scrollForTrackPage, scrollbarGeometry, scrollbarPresentation, scrollbarReservesSpace, scrollbarSelectionRows, scrollbarWheelRows, } from "./scrollbar.js";
25
25
  export type { RailPointer, RailPosition, ScrollbarAppearance, ScrollbarGeometry, ScrollbarInput, ScrollbarPresentation, ScrollbarPresentationInput, ScrollbarSpeed, ScrollbarStyle, } from "./scrollbar.js";
26
- export { renderShortcutHints, renderShortcutHintsWithClose } from "./shortcut-hints.js";
26
+ export { assertOwnedShortcutHintConventions, renderShortcutHints, renderShortcutHintsWithClose, shortcutHintsText } from "./shortcut-hints.js";
27
27
  export type { ShortcutHintEntry } from "./shortcut-hints.js";
28
28
  export { GLOBAL_SCOPE, ShortcutRegistry, assembleShortcuts, assertNoShortcutConflicts } from "./shortcuts.js";
29
29
  export type { ShortcutConflict, ShortcutDeclaration, ShortcutRegistryResult } from "./shortcuts.js";
@@ -1,4 +1,4 @@
1
- export { dialogRowAt, dialogValueColumn, renderDialogPanel } from "./dialog-panel.js";
1
+ export { dialogRowAt, dialogValueColumn, renderDialogPanel, renderSteppedDialogPanel } from "./dialog-panel.js";
2
2
  export { FrameContractError, assertPaneRect, finalizeFrame, validateFrame } from "./frame.js";
3
3
  export { humanizeLabel, humanizeTitle } from "./label.js";
4
4
  export { LineInput, caretCell, handleLineInputKey, renderInputRow, wordLeft, wordRight } from "./line-input.js";
@@ -11,7 +11,7 @@ export { progressStatusFrame, progressStatusText } from "./progress-status.js";
11
11
  export { FrameCache, RENDER_REVISION_KINDS, RenderRevisionTracker, ZERO_REVISIONS, normalizeRevisions, revisionsEqual } from "./revision.js";
12
12
  export { MAX_COPY_ROWS, MAX_COPY_SOURCE_UNITS, selectionCopyLineContent, selectionCopyRowText } from "./selection-copy.js";
13
13
  export { ScrollbarRails, isThumbRow, scrollForThumbRow, scrollForTrackPage, scrollbarGeometry, scrollbarPresentation, scrollbarReservesSpace, scrollbarSelectionRows, scrollbarWheelRows, } from "./scrollbar.js";
14
- export { renderShortcutHints, renderShortcutHintsWithClose } from "./shortcut-hints.js";
14
+ export { assertOwnedShortcutHintConventions, renderShortcutHints, renderShortcutHintsWithClose, shortcutHintsText } from "./shortcut-hints.js";
15
15
  export { GLOBAL_SCOPE, ShortcutRegistry, assembleShortcuts, assertNoShortcutConflicts } from "./shortcuts.js";
16
16
  export { backgroundSgrSpan, heldNativeHyperlinkStyle, hyperlinkSgrSpan, hyperlinkTargetAtColumn, nativeHyperlinkStyle, overlaySpan } from "./spans.js";
17
17
  export { readVisibleHyperlinks } from "./visible-hyperlinks.js";
@@ -58,6 +58,8 @@ export interface InputRowOptions {
58
58
  readonly placeholder?: string;
59
59
  /** Rules above and below, in the prompt's own grey. Default true. */
60
60
  readonly ruled?: boolean;
61
+ /** Prompt marker including any following spacing. Defaults to the shared prompt glyph. */
62
+ readonly promptGlyph?: string;
61
63
  readonly theme?: Pick<UiTheme, "fg">;
62
64
  }
63
65
  export interface InputRow {