@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
@@ -1,9 +1,9 @@
1
1
  import { DynamicBorder } from "../startup-public.js";
2
2
  import { Container, getKeybindings, Input, Spacer, Text } from "@earendil-works/pi-tui";
3
3
  import { addPiModalHeader, adoptPiModalFrame } from "./modal-frame.js";
4
- import { PINNED_PI_LAYOUT, piTheme, renderPiModalShortcutHints } from "./theme.js";
4
+ import { DIALOG_CLOSE_SHORTCUT_HINT, PINNED_PI_LAYOUT, piTheme, renderPiModalListRow, renderPiModalShortcutHints, } from "./theme.js";
5
5
  import { componentPort, ensureTheme, piShellTruncateToWidth, piShellVisibleWidth } from "./shell-shared-facade.js";
6
- import { SKILL_COMMAND_PREFIX, skillMatchesQuery } from "./skills-command.js";
6
+ import { skillMatchesQuery } from "./skills-command.js";
7
7
  /** A single line clipped at the viewport width: no wrap, no ellipsis, padded to the full width. */
8
8
  class ClippedLine {
9
9
  #text;
@@ -14,10 +14,21 @@ class ClippedLine {
14
14
  return [line + " ".repeat(Math.max(0, width - piShellVisibleWidth(line)))];
15
15
  }
16
16
  }
17
+ class SkillRow {
18
+ #content;
19
+ #selected;
20
+ constructor(content, selected) {
21
+ this.#content = content;
22
+ this.#selected = selected;
23
+ }
24
+ invalidate() { }
25
+ render(width) {
26
+ return [renderPiModalListRow(this.#content, width, this.#selected)];
27
+ }
28
+ }
17
29
  /**
18
- * The A1-owned searchable Skills dialog. Its composition is the model selector's (border, spacer,
19
- * search input, spacer, list, spacer, hint footer, border) built from public pi-tui components; its
20
- * content follows the v2 skills extension: an accent bold title, `skill:<name>` rows, the selected
30
+ * The A1-owned searchable Skills dialog uses the model selector's public pi-tui composition; its
31
+ * content follows the v2 skills extension: an accent bold title, bare skill-name rows, the selected
21
32
  * skill's description cut to one line under the rows, the pinned scroll counter on overflow,
22
33
  * and the two empty states. Enter applies the selected skill with no arguments; the query is never appended.
23
34
  */
@@ -45,9 +56,10 @@ class SkillsSelectorComponent extends Container {
45
56
  this.addChild(new Spacer(1));
46
57
  const bindings = getKeybindings();
47
58
  this.addChild(new Text(renderPiModalShortcutHints([
59
+ { key: "Type", action: "search" },
48
60
  { key: "↑↓", action: "navigate" },
49
61
  { key: bindings.getKeys("tui.select.confirm").join("/"), action: "select" },
50
- { key: bindings.getKeys("tui.select.cancel").join("/"), action: "cancel" },
62
+ DIALOG_CLOSE_SHORTCUT_HINT,
51
63
  ]), 0, 0));
52
64
  this.addChild(new DynamicBorder());
53
65
  adoptPiModalFrame(this, { topIndex: 0, bottomIndex: this.children.length - 1, header });
@@ -108,8 +120,9 @@ class SkillsSelectorComponent extends Container {
108
120
  for (let index = start; index < end; index++) {
109
121
  const skill = this.#filtered[index];
110
122
  const selected = index === this.#selectedIndex;
111
- const label = `${SKILL_COMMAND_PREFIX}${skill.name}`;
112
- this.#listContainer.addChild(new Text(`${selected ? theme.fg("accent", "→ ") : " "}${selected ? theme.fg("accent", label) : label}`, 0, 0));
123
+ const label = skill.name;
124
+ const content = `${selected ? theme.fg("accent", "→ ") : " "}${selected ? theme.fg("text", label) : label}`;
125
+ this.#listContainer.addChild(new SkillRow(content, selected));
113
126
  }
114
127
  if (start > 0 || end < this.#filtered.length) {
115
128
  this.#listContainer.addChild(new Text(theme.fg("muted", ` (${this.#selectedIndex + 1}/${this.#filtered.length})`), 0, 0));
@@ -1,5 +1,12 @@
1
- import { type SemanticShortcutHint } from "../../../contracts/presentation/index.js";
1
+ import { DIALOG_CLOSE_SHORTCUT_HINT, displayShortcutKeyLabel, type SemanticShortcutHint } from "../../../contracts/presentation/index.js";
2
2
  export * from "./upstream/theme/theme.js";
3
3
  export type PiModalShortcutHint = SemanticShortcutHint;
4
+ export { DIALOG_CLOSE_SHORTCUT_HINT, displayShortcutKeyLabel };
5
+ /** Paint one selected bare-A1 list item with the standard blue selection surface. */
6
+ export declare function renderPiListSelection(content: string): string;
7
+ /** One standard bare-A1 modal row, clipped before its item-bounded selection is painted. */
8
+ export declare function renderPiModalListRow(content: string, width: number, selected: boolean): string;
4
9
  /** The common bare-A1 modal footer: quiet display-cased keys, muted names, and whitespace-only gaps. */
5
10
  export declare function renderPiModalShortcutHints(entries: readonly PiModalShortcutHint[], indent?: number): string;
11
+ /** Adapts Pi's closed settings footer while reserving the canonical close suffix. */
12
+ export declare function canonicalizePiSettingsHint(row: string, width: number): string;
@@ -1,6 +1,17 @@
1
- import { renderSemanticShortcutHints, } from "../../../contracts/presentation/index.js";
1
+ import { DIALOG_CLOSE_SHORTCUT_HINT, displayShortcutKeyLabel, renderSemanticShortcutHints, } from "../../../contracts/presentation/index.js";
2
+ import { stripTerminalSequences, truncateToWidth } from "@earendil-works/pi-tui";
2
3
  import { piTheme } from "./upstream/theme/theme.js";
3
4
  export * from "./upstream/theme/theme.js";
5
+ export { DIALOG_CLOSE_SHORTCUT_HINT, displayShortcutKeyLabel };
6
+ /** Paint one selected bare-A1 list item with the standard blue selection surface. */
7
+ export function renderPiListSelection(content) {
8
+ return piTheme().bg("selectedBg", content);
9
+ }
10
+ /** One standard bare-A1 modal row, clipped before its item-bounded selection is painted. */
11
+ export function renderPiModalListRow(content, width, selected) {
12
+ const clipped = truncateToWidth(content, Math.max(0, width), "");
13
+ return selected ? renderPiListSelection(clipped) : clipped;
14
+ }
4
15
  /** The common bare-A1 modal footer: quiet display-cased keys, muted names, and whitespace-only gaps. */
5
16
  export function renderPiModalShortcutHints(entries, indent = 0) {
6
17
  const theme = piTheme();
@@ -9,3 +20,19 @@ export function renderPiModalShortcutHints(entries, indent = 0) {
9
20
  action: text => theme.fg("muted", text),
10
21
  }, indent);
11
22
  }
23
+ /** Adapts Pi's closed settings footer while reserving the canonical close suffix. */
24
+ export function canonicalizePiSettingsHint(row, width) {
25
+ const plain = stripTerminalSequences(row);
26
+ const body = plain.includes("Type to search") ? " Type to search · Enter/Space to change"
27
+ : plain.includes("Enter/Space to change") ? " Enter/Space to change"
28
+ : plain.includes("Type to filter") ? " Type to filter · Enter to select"
29
+ : plain.includes("Enter to select") && plain.includes("Esc") ? " Enter to select"
30
+ : /^\s*(?:Type to|Enter(?:\/Space)? to)/u.test(plain) ? plain.replace(/\.{3}$/u, "") : null;
31
+ if (body === null)
32
+ return row;
33
+ const close = `${displayShortcutKeyLabel(DIALOG_CLOSE_SHORTCUT_HINT.key)} ${DIALOG_CLOSE_SHORTCUT_HINT.action}`;
34
+ if (width <= close.length)
35
+ return piTheme().fg("dim", truncateToWidth(close, width, ""));
36
+ const bodyWidth = width - close.length - 3;
37
+ return piTheme().fg("dim", bodyWidth <= 0 ? close : `${truncateToWidth(body, bodyWidth, "…")} · ${close}`);
38
+ }
@@ -3,9 +3,11 @@
3
3
  * packages/coding-agent/src/core/keybindings.ts.
4
4
  * Modifications: Mechanical source port with Node import prefixes, public package-root agent-directory
5
5
  * resolution, and an opt-in bare-A1 input profile including Ctrl+L level cycling, unbound model
6
- * selection, and cross-platform Alt+Up queued-message restoration.
6
+ * selection, Tab tree-filter cycling, tree-local navigation aliases, cross-platform Alt+Up
7
+ * queued-message restoration, and an implicit Ctrl+C selection-cancel alias omitted from shortcut
8
+ * labels.
7
9
  * Deviations: keybindings-public-config-boundary, owned-level-cycle-shortcut,
8
- * owned-input-keybinding-aliases.
10
+ * owned-input-keybinding-aliases, owned-session-tree-dialog, owned-dialog-ctrl-c-cancel.
9
11
  */
10
12
  import { type Keybinding, type KeybindingDefinitions, type KeybindingsConfig, type KeyId, KeybindingsManager as TuiKeybindingsManager } from "@earendil-works/pi-tui";
11
13
  export interface AppKeybindings {
@@ -58,6 +60,10 @@ export interface AppKeybindings {
58
60
  "owned.editor.redo": true;
59
61
  "owned.editor.extendLeft": true;
60
62
  "owned.editor.extendRight": true;
63
+ "owned.tree.collapse": true;
64
+ "owned.tree.expand": true;
65
+ "owned.tree.first": true;
66
+ "owned.tree.last": true;
61
67
  }
62
68
  export type AppKeybinding = keyof AppKeybindings;
63
69
  export declare function useWindowsKeybindings(platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): boolean;
@@ -437,6 +443,8 @@ export declare class KeybindingsManager extends TuiKeybindingsManager {
437
443
  static create(agentDir?: string): KeybindingsManager;
438
444
  static fromOwnedBindings(bindings?: KeybindingsConfig): KeybindingsManager;
439
445
  static createForOwnedInput(agentDir?: string): KeybindingsManager;
446
+ matches(data: string, keybinding: Keybinding): boolean;
447
+ getKeys(keybinding: Keybinding): KeyId[];
440
448
  private static createWithDefinitions;
441
449
  reload(): void;
442
450
  getEffectiveConfig(): KeybindingsConfig;
@@ -3,11 +3,13 @@
3
3
  * packages/coding-agent/src/core/keybindings.ts.
4
4
  * Modifications: Mechanical source port with Node import prefixes, public package-root agent-directory
5
5
  * resolution, and an opt-in bare-A1 input profile including Ctrl+L level cycling, unbound model
6
- * selection, and cross-platform Alt+Up queued-message restoration.
6
+ * selection, Tab tree-filter cycling, tree-local navigation aliases, cross-platform Alt+Up
7
+ * queued-message restoration, and an implicit Ctrl+C selection-cancel alias omitted from shortcut
8
+ * labels.
7
9
  * Deviations: keybindings-public-config-boundary, owned-level-cycle-shortcut,
8
- * owned-input-keybinding-aliases.
10
+ * owned-input-keybinding-aliases, owned-session-tree-dialog, owned-dialog-ctrl-c-cancel.
9
11
  */
10
- import { TUI_KEYBINDINGS, KeybindingsManager as TuiKeybindingsManager, } from "@earendil-works/pi-tui";
12
+ import { matchesKey, TUI_KEYBINDINGS, KeybindingsManager as TuiKeybindingsManager, } from "@earendil-works/pi-tui";
11
13
  import { existsSync, readFileSync } from "node:fs";
12
14
  import { join } from "node:path";
13
15
  import { getAgentDir } from "../../../../startup-public.js";
@@ -185,6 +187,7 @@ const OWNED_INPUT_KEYBINDINGS = {
185
187
  "app.thinking.cycle": { ...KEYBINDINGS["app.thinking.cycle"], defaultKeys: "ctrl+l" },
186
188
  "app.model.select": { ...KEYBINDINGS["app.model.select"], defaultKeys: [] },
187
189
  "app.message.dequeue": { ...KEYBINDINGS["app.message.dequeue"], defaultKeys: "alt+up" },
190
+ "app.tree.filter.cycleForward": { ...KEYBINDINGS["app.tree.filter.cycleForward"], defaultKeys: "tab" },
188
191
  "tui.editor.deleteWordBackward": {
189
192
  ...KEYBINDINGS["tui.editor.deleteWordBackward"],
190
193
  defaultKeys: [...KEYBINDINGS["tui.editor.deleteWordBackward"].defaultKeys, "ctrl+backspace"],
@@ -199,15 +202,17 @@ const OWNED_INPUT_KEYBINDINGS = {
199
202
  },
200
203
  // Ctrl+Z edits the prompt in bare A1 instead of suspending the process.
201
204
  "app.suspend": { ...KEYBINDINGS["app.suspend"], defaultKeys: [] },
202
- // Owned prompt-selection actions are intercepted before vanilla Pi editor
203
- // actions. Keeping them in the keybinding manager makes the UX declarative,
204
- // configurable, and independent of terminal escape-sequence spellings.
205
+ // Owned prompt-selection actions remain declarative and take priority.
205
206
  "owned.editor.selectAll": { defaultKeys: "ctrl+a", description: "Select all prompt text" },
206
207
  "owned.editor.cut": { defaultKeys: "ctrl+x", description: "Cut selected prompt text" },
207
208
  "owned.editor.paste": { defaultKeys: "ctrl+v", description: "Paste clipboard text" },
208
209
  "owned.editor.redo": { defaultKeys: "ctrl+y", description: "Redo prompt edit" },
209
210
  "owned.editor.extendLeft": { defaultKeys: "shift+left", description: "Extend prompt selection left" },
210
211
  "owned.editor.extendRight": { defaultKeys: "shift+right", description: "Extend prompt selection right" },
212
+ "owned.tree.collapse": { defaultKeys: "left", description: "Collapse selected tree branch" },
213
+ "owned.tree.expand": { defaultKeys: "right", description: "Expand selected tree branch" },
214
+ "owned.tree.first": { defaultKeys: "home", description: "Select first tree entry" },
215
+ "owned.tree.last": { defaultKeys: "end", description: "Select last tree entry" },
211
216
  };
212
217
  const KEYBINDING_NAME_MIGRATIONS = {
213
218
  cursorUp: "tui.editor.cursorUp",
@@ -349,6 +354,15 @@ export class KeybindingsManager extends TuiKeybindingsManager {
349
354
  static createForOwnedInput(agentDir = getAgentDir()) {
350
355
  return KeybindingsManager.createWithDefinitions(agentDir, OWNED_INPUT_KEYBINDINGS);
351
356
  }
357
+ // Protocol: Ctrl+C is a conventional dialog escape hatch, not advertised keybinding chrome.
358
+ matches(data, keybinding) {
359
+ return keybinding === "tui.select.cancel" && matchesKey(data, "ctrl+c")
360
+ || super.matches(data, keybinding);
361
+ }
362
+ getKeys(keybinding) {
363
+ const keys = super.getKeys(keybinding);
364
+ return keybinding === "tui.select.cancel" ? keys.filter(key => key !== "ctrl+c") : keys;
365
+ }
352
366
  static createWithDefinitions(agentDir, definitions) {
353
367
  const configPath = join(agentDir, "keybindings.json");
354
368
  const userBindings = KeybindingsManager.loadFromFile(configPath);
@@ -11,7 +11,7 @@ import { Container, Editor, Spacer, Text, } from "@earendil-works/pi-tui";
11
11
  import { DynamicBorder, getSelectListTheme } from "../../../startup-public.js";
12
12
  import { editInExternalEditor } from "../external-editor.js";
13
13
  import { addPiModalHeader, adoptPiModalFrame } from "../../modal-frame.js";
14
- import { piTheme, renderPiModalShortcutHints } from "../../theme.js";
14
+ import { DIALOG_CLOSE_SHORTCUT_HINT, piTheme, renderPiModalShortcutHints } from "../../theme.js";
15
15
  export class ExtensionEditorComponent extends Container {
16
16
  #editor;
17
17
  #tui;
@@ -54,8 +54,8 @@ export class ExtensionEditorComponent extends Container {
54
54
  const hint = renderPiModalShortcutHints([
55
55
  { key: this.#keybindings.getKeys("tui.select.confirm").join("/"), action: "submit" },
56
56
  { key: this.#keybindings.getKeys("tui.input.newLine").join("/"), action: "newline" },
57
- { key: this.#keybindings.getKeys("tui.select.cancel").join("/"), action: "cancel" },
58
57
  { key: this.#keybindings.getKeys("app.editor.external").join("/"), action: "external editor" },
58
+ DIALOG_CLOSE_SHORTCUT_HINT,
59
59
  ]);
60
60
  this.addChild(new Text(hint, 0, 0));
61
61
  this.addChild(new Spacer(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,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");
@@ -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;