@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
@@ -0,0 +1,40 @@
1
+ import { CancellableLoader, Spacer, Text, } from "@earendil-works/pi-tui";
2
+ import { BorderedLoader, DynamicBorder } from "../startup-public.js";
3
+ import { adoptPiModalFrame, PiModalFrame } from "./modal-frame.js";
4
+ import { DIALOG_CLOSE_SHORTCUT_HINT, piTheme, renderPiModalShortcutHints } from "./theme.js";
5
+ import { componentPort, createTuiFacade, ensureTheme, } from "./shell-shared-facade.js";
6
+ /** Bare A1's titled share progress surface; the pinned comparison keeps its bordered loader. */
7
+ class ShareOperationDialog {
8
+ #loader;
9
+ #frame;
10
+ constructor(runtime, message) {
11
+ const theme = piTheme();
12
+ this.#loader = new CancellableLoader(createTuiFacade(runtime), text => theme.fg("accent", text), text => theme.fg("muted", text), message);
13
+ this.#frame = new PiModalFrame(new DynamicBorder(text => theme.fg("border", text)), [
14
+ new Text(theme.fg("accent", theme.bold("Share")), 0, 0),
15
+ this.#loader,
16
+ new Spacer(1),
17
+ new Text(renderPiModalShortcutHints([DIALOG_CLOSE_SHORTCUT_HINT]), 0, 0),
18
+ ], new DynamicBorder(text => theme.fg("border", text)));
19
+ }
20
+ get signal() { return this.#loader.signal; }
21
+ invalidate() { this.#frame.invalidate(); }
22
+ render(width) { return this.#frame.render(width); }
23
+ handleInput(data) { this.#loader.handleInput(data); }
24
+ dispose() { this.#loader.dispose(); }
25
+ }
26
+ export function createPiShellShareOperationDialog(runtime, message) {
27
+ ensureTheme();
28
+ const dialog = new ShareOperationDialog(runtime, message);
29
+ return { ...componentPort(dialog), signal: dialog.signal };
30
+ }
31
+ export function createPiShellOperationLoader(runtime, message) {
32
+ ensureTheme();
33
+ const loader = new BorderedLoader(createTuiFacade(runtime), piTheme(), message, { cancellable: true });
34
+ adoptPiModalFrame(loader, {
35
+ topIndex: 0,
36
+ bottomIndex: loader.children.length - 1,
37
+ preInsetContent: loader.children[3] === undefined ? [] : [loader.children[3]],
38
+ });
39
+ return { ...componentPort(loader), signal: loader.signal };
40
+ }
@@ -4,11 +4,21 @@ import { PROMPT_HISTORY_EDITOR_REPLACEMENT } from "../../../contracts/owned-ui/i
4
4
  import { KeybindingsManager } from "./upstream/adjacent/core/keybindings.js";
5
5
  import { OwnedEditor } from "./upstream/components/owned-editor.js";
6
6
  import { OwnedEditorUxInterception, createPromptSelectionInterceptor, editorVisualLineCount, } from "./owned-editor-ux.js";
7
- import { PINNED_PI_LAYOUT, piTheme, } from "./theme.js";
7
+ import { PINNED_PI_LAYOUT, piTheme, renderPiListSelection, } from "./theme.js";
8
8
  import { createTuiFacade, ensureTheme, isAutocompleteProvider, } from "./shell-shared-facade.js";
9
9
  import { SKILLS_COMMAND_NAME, collapseSkillCommands, createSkillsTunnelProvider, } from "./skills-command.js";
10
- /** A selected autocomplete row: the accent primary column, then the aligned description. */
10
+ /** A selected autocomplete row: its arrow/primary column, then the aligned description. */
11
11
  const SELECTED_DESCRIBED_ROW = /^(→ .*?\S)(\s{2,}.*)$/u;
12
+ function renderOwnedAutocompleteSelection(text) {
13
+ const theme = piTheme();
14
+ const row = SELECTED_DESCRIBED_ROW.exec(text);
15
+ const primary = row?.[1] ?? text;
16
+ const hasArrow = primary.startsWith("→ ");
17
+ const arrow = hasArrow ? theme.fg("accent", "→ ") : "";
18
+ const label = theme.fg("text", hasArrow ? primary.slice(2) : primary);
19
+ const description = row === null ? "" : theme.fg("muted", row[2]);
20
+ return renderPiListSelection(arrow + label + description);
21
+ }
12
22
  const PINNED_THINKING_SLASH_COMMAND = { name: "thinking", description: "Set thinking level", argumentHint: "<level>" };
13
23
  const OWNED_THINKING_SLASH_COMMAND = { name: "thinking", description: "Set thinking level" };
14
24
  const OWNED_LOGIN_SLASH_COMMAND = { name: "login", description: "Configure provider authentication" };
@@ -74,10 +84,9 @@ export function createPiShellEditor(options) {
74
84
  return selectListTheme.scrollInfo(text);
75
85
  },
76
86
  selectedText: text => {
77
- // Rationale: the pinned list styles a whole selected row at once. Bare A1 keeps the
78
- // actionable primary column accented while every aligned description remains muted.
79
- const row = SELECTED_DESCRIBED_ROW.exec(text);
80
- return row === null ? selectListTheme.selectedText(text) : selectListTheme.selectedText(row[1]) + selectListTheme.description(row[2]);
87
+ // Rationale: bare A1 keeps the arrow accented, the actionable label readable, and descriptions muted
88
+ // on the same item-bounded blue surface used by its standard modal selectors.
89
+ return renderOwnedAutocompleteSelection(text);
81
90
  },
82
91
  },
83
92
  }, keybindings, {
@@ -28,6 +28,9 @@ export interface PiExtensionUiBridgeHost {
28
28
  }
29
29
  export interface PiExtensionUiBridge {
30
30
  readonly context: ExtensionUIContext;
31
+ input(title: string, placeholder: string, options?: {
32
+ readonly retainSurfaceOnSettle?: boolean;
33
+ }): Promise<string | undefined>;
31
34
  reset(): void;
32
35
  dispose(): void;
33
36
  }
@@ -5,7 +5,7 @@ import { ExtensionSelectorComponent } from "./upstream/components/extension-sele
5
5
  import { setKeybindings, Text, } from "@earendil-works/pi-tui";
6
6
  import { KeybindingsManager, } from "./upstream/adjacent/core/keybindings.js";
7
7
  import { PINNED_PI_LAYOUT, applyPiTheme, applyPiThemeInstance, getAvailablePiThemes, loadPiTheme, piTheme, } from "./theme.js";
8
- import { componentPort, createTuiFacade, ensureTheme, isRecord, } from "./shell-shared-facade.js";
8
+ import { componentPort, createTuiFacade, ensureTheme, isRecord, withPiDialogCancel, } from "./shell-shared-facade.js";
9
9
  /** Public ExtensionUIContext ported from pinned InteractiveMode without constructing it. */
10
10
  export function createPiExtensionUiBridge(host) {
11
11
  ensureTheme();
@@ -45,7 +45,7 @@ export function createPiExtensionUiBridge(host) {
45
45
  disposers.delete(dispose);
46
46
  };
47
47
  };
48
- const showInput = (create, options) => new Promise(resolve => {
48
+ const showInput = (create, options, retainSurfaceOnSettle = false) => new Promise(resolve => {
49
49
  let settled = false;
50
50
  let surface;
51
51
  let untrack = () => { };
@@ -56,7 +56,10 @@ export function createPiExtensionUiBridge(host) {
56
56
  if (activeCancel === cancel)
57
57
  activeCancel = undefined;
58
58
  untrack();
59
- closeSurface(surface);
59
+ if (retainSurfaceOnSettle && activeSurface === surface)
60
+ activeSurface = undefined;
61
+ else
62
+ closeSurface(surface);
60
63
  resolve(value);
61
64
  };
62
65
  const cancel = () => finish(undefined);
@@ -173,7 +176,7 @@ export function createPiExtensionUiBridge(host) {
173
176
  component.dispose();
174
177
  return;
175
178
  }
176
- surface = componentPort(component);
179
+ surface = withPiDialogCancel(componentPort(component), cancelCustom);
177
180
  activeCancel = cancelCustom;
178
181
  if (options?.overlay) {
179
182
  const overlayOptions = typeof options.overlayOptions === "function" ? options.overlayOptions() : options.overlayOptions;
@@ -247,6 +250,7 @@ export function createPiExtensionUiBridge(host) {
247
250
  };
248
251
  return {
249
252
  context,
253
+ input: (title, placeholder, options) => showInput((resolve, cancel) => componentPort(new ExtensionInputComponent(title, placeholder, resolve, cancel, { tui })), undefined, options?.retainSurfaceOnSettle === true),
250
254
  reset,
251
255
  dispose() {
252
256
  reset();
@@ -110,10 +110,6 @@ export interface PiShellLoginDialogPort extends PiShellComponentPort {
110
110
  export declare function createPiShellLoginDialog(runtime: Pick<PiShellEditorOptions, "getColumns" | "getRows" | "requestRender">, providerId: string, onComplete: (success: boolean, message?: string) => void, providerName?: string, title?: string): PiShellLoginDialogPort;
111
111
  export declare function createPiShellArmin(runtime: Pick<PiShellEditorOptions, "getColumns" | "getRows" | "requestRender">): PiShellComponentPort;
112
112
  export declare function createPiShellEarendilAnnouncement(): PiShellComponentPort;
113
- export interface PiShellOperationLoaderPort extends PiShellComponentPort {
114
- readonly signal: AbortSignal;
115
- }
116
- export declare function createPiShellOperationLoader(runtime: Pick<PiShellEditorOptions, "getColumns" | "getRows" | "requestRender">, message: string): PiShellOperationLoaderPort;
117
113
  export declare function createPiShellReloadBox(): PiShellComponentPort;
118
114
  export interface PiShellAuthProviderOption extends PiShellSelectorOption {
119
115
  readonly providerId: string;
@@ -1,11 +1,12 @@
1
- import { ArminComponent, BorderedLoader, DynamicBorder, ExtensionSelectorComponent, getSelectListTheme, LoginDialogComponent, ModelSelectorComponent, OAuthSelectorComponent, SettingsSelectorComponent, ShowImagesSelectorComponent, ThemeSelectorComponent, UserMessageSelectorComponent, } from "../startup-public.js";
2
- import { EarendilAnnouncementComponent, } from "./upstream/components/earendil-announcement.js";
1
+ import { ArminComponent, DynamicBorder, getSelectListTheme, LoginDialogComponent, ModelSelectorComponent, OAuthSelectorComponent, SettingsSelectorComponent, ShowImagesSelectorComponent, ThemeSelectorComponent, UserMessageSelectorComponent, } from "../startup-public.js";
2
+ import { EarendilAnnouncementComponent } from "./upstream/components/earendil-announcement.js";
3
+ import { ExtensionSelectorComponent } from "./upstream/components/extension-selector.js";
3
4
  import { Box, Container, SelectList, Spacer, Text, } from "@earendil-works/pi-tui";
4
- import { ScopedModelsSelectorComponent, } from "./upstream/components/scoped-models-selector.js";
5
- import { adoptPiModalFrame, adoptPiModalHeader } from "./modal-frame.js";
6
- import { ModelsDialogComponent, } from "./models-dialog.js";
7
- import { TrustSelectorComponent, } from "./upstream/components/trust-selector.js";
8
- import { PINNED_PI_LAYOUT, piTheme, } from "./theme.js";
5
+ import { ScopedModelsSelectorComponent } from "./upstream/components/scoped-models-selector.js";
6
+ import { addPiModalSelectFooter, adoptPiModalFrame, adoptPiModalHeader } from "./modal-frame.js";
7
+ import { ModelsDialogComponent } from "./models-dialog.js";
8
+ import { TrustSelectorComponent } from "./upstream/components/trust-selector.js";
9
+ import { canonicalizePiSettingsHint, DIALOG_CLOSE_SHORTCUT_HINT, PINNED_PI_LAYOUT, piTheme, renderPiModalShortcutHints, } from "./theme.js";
9
10
  import { componentFromPort, componentPort, createTuiFacade, ensureTheme, isRecord, } from "./shell-shared-facade.js";
10
11
  export { createPiShellSkillsSelector } from "./skills-dialog.js";
11
12
  export function createPiShellSelector(options) {
@@ -16,11 +17,20 @@ export function createPiShellSelector(options) {
16
17
  list.onSelect = item => options.onSelect?.(item.value);
17
18
  if (options.onCancel !== undefined)
18
19
  list.onCancel = options.onCancel;
19
- if (!options.title)
20
+ if (!options.title && options.onCancel === undefined)
20
21
  return componentPort(list);
21
22
  const container = new Container();
22
- container.addChild(new Text(piTheme().fg("accent", piTheme().bold(options.title)), PINNED_PI_LAYOUT.contentPaddingX, 0));
23
+ if (options.title)
24
+ container.addChild(new Text(piTheme().fg("accent", piTheme().bold(options.title)), PINNED_PI_LAYOUT.contentPaddingX, 0));
23
25
  container.addChild(list);
26
+ if (options.onCancel !== undefined) {
27
+ container.addChild(new Spacer(1));
28
+ container.addChild(new Text(renderPiModalShortcutHints([
29
+ { key: "↑↓", action: "navigate" },
30
+ { key: "enter", action: "select" },
31
+ DIALOG_CLOSE_SHORTCUT_HINT,
32
+ ]), 0, 0));
33
+ }
24
34
  return componentPort(container, data => list.handleInput(data));
25
35
  }
26
36
  export function createPiShellSettingsSelector(options) {
@@ -68,7 +78,19 @@ export function createPiShellSettingsSelector(options) {
68
78
  };
69
79
  const selector = new SettingsSelectorComponent(options.config, callbacks);
70
80
  const settingsList = selector.getSettingsList();
71
- return componentPort(selector, data => settingsList.handleInput(data));
81
+ const port = componentPort(selector, data => settingsList.handleInput(data));
82
+ return {
83
+ ...port,
84
+ // Rationale: Pi's nested settings components own their hint rows and do not expose hint entries.
85
+ // Rebuild only the footer row so the canonical close entry survives narrow clipping.
86
+ render(width) {
87
+ const rows = [...port.render(width)];
88
+ const hintIndex = rows.length - 2;
89
+ if (rows[hintIndex] !== undefined)
90
+ rows[hintIndex] = canonicalizePiSettingsHint(rows[hintIndex], width);
91
+ return rows;
92
+ },
93
+ };
72
94
  }
73
95
  export function createPiShellModelSelector(options) {
74
96
  ensureTheme();
@@ -139,6 +161,7 @@ export function createPiShellUserMessageSelector(messages, onSelect, onCancel, i
139
161
  ensureTheme();
140
162
  const selector = new UserMessageSelectorComponent(messages.map(message => ({ id: message.id, text: message.label })), onSelect, onCancel, initialSelectedId);
141
163
  const list = selector.getMessageList();
164
+ addPiModalSelectFooter(selector);
142
165
  adoptPiModalFrame(selector, { topIndex: 4, bottomIndex: selector.children.length - 1 });
143
166
  return componentPort(selector, data => list.handleInput(data));
144
167
  }
@@ -146,6 +169,9 @@ export function createPiShellLoginDialog(runtime, providerId, onComplete, provid
146
169
  ensureTheme();
147
170
  const dialog = new LoginDialogComponent(createTuiFacade(runtime), providerId, onComplete, providerName, title);
148
171
  const preInsetTitle = dialog.children[1];
172
+ const content = dialog.children[2];
173
+ dialog.children.splice(dialog.children.length - 1, 0, new Spacer(1), new Text(renderPiModalShortcutHints([DIALOG_CLOSE_SHORTCUT_HINT]), 0, 0));
174
+ const removeDependencyHint = () => { content.children.pop(); };
149
175
  const header = adoptPiModalHeader(dialog, 0, 1);
150
176
  adoptPiModalFrame(dialog, {
151
177
  topIndex: 0,
@@ -157,11 +183,23 @@ export function createPiShellLoginDialog(runtime, providerId, onComplete, provid
157
183
  ...componentPort(dialog),
158
184
  showAuth: (url, instructions) => dialog.showAuth(url, instructions),
159
185
  showDeviceCode: info => dialog.showDeviceCode(info),
160
- showManualInput: message => dialog.showManualInput(message),
161
- showPrompt: (message, placeholder) => dialog.showPrompt(message, placeholder),
186
+ showManualInput(message) {
187
+ const result = dialog.showManualInput(message);
188
+ removeDependencyHint();
189
+ return result;
190
+ },
191
+ showPrompt(message, placeholder) {
192
+ const result = dialog.showPrompt(message, placeholder);
193
+ removeDependencyHint();
194
+ return result;
195
+ },
162
196
  showDetails: lines => dialog.showDetails([...lines]),
163
- showInfo: (message, links = [], showCloseHint = false) => dialog.showInfo(message, [...links], showCloseHint),
164
- showWaiting: message => dialog.showWaiting(message),
197
+ // Rationale: The persistent canonical footer replaces the dependency's optional close hint.
198
+ showInfo: (message, links = []) => dialog.showInfo(message, [...links], false),
199
+ showWaiting(message) {
200
+ dialog.showWaiting(message);
201
+ removeDependencyHint();
202
+ },
165
203
  showProgress: message => dialog.showProgress(message),
166
204
  };
167
205
  }
@@ -173,16 +211,6 @@ export function createPiShellEarendilAnnouncement() {
173
211
  ensureTheme();
174
212
  return componentPort(new EarendilAnnouncementComponent());
175
213
  }
176
- export function createPiShellOperationLoader(runtime, message) {
177
- ensureTheme();
178
- const loader = new BorderedLoader(createTuiFacade(runtime), piTheme(), message, { cancellable: true });
179
- adoptPiModalFrame(loader, {
180
- topIndex: 0,
181
- bottomIndex: loader.children.length - 1,
182
- preInsetContent: loader.children[3] === undefined ? [] : [loader.children[3]],
183
- });
184
- return { ...componentPort(loader), signal: loader.signal };
185
- }
186
214
  export function createPiShellReloadBox() {
187
215
  ensureTheme();
188
216
  const container = new Container();
@@ -207,6 +235,7 @@ export function createPiShellAuthProviderSelector(mode, providers, onSelect, onC
207
235
  if (selected)
208
236
  onSelect(selected.id);
209
237
  }, onCancel, initialSearchInput);
238
+ addPiModalSelectFooter(selector);
210
239
  const preInsetContent = [selector.children[2], selector.children[6]];
211
240
  const header = adoptPiModalHeader(selector, 0, 2);
212
241
  adoptPiModalFrame(selector, {
@@ -219,21 +248,13 @@ export function createPiShellAuthProviderSelector(mode, providers, onSelect, onC
219
248
  }
220
249
  export function createPiShellExtensionSelector(title, options, onSelect, onCancel) {
221
250
  ensureTheme();
222
- const selector = new ExtensionSelectorComponent(title, [...options], onSelect, onCancel);
223
- const preInsetContent = [selector.children[2], selector.children[4], selector.children[6]];
224
- const header = adoptPiModalHeader(selector, 0, 2);
225
- adoptPiModalFrame(selector, {
226
- topIndex: 0,
227
- bottomIndex: selector.children.length - 1,
228
- header,
229
- preInsetContent,
230
- });
231
- return componentPort(selector);
251
+ return componentPort(new ExtensionSelectorComponent(title, [...options], onSelect, onCancel));
232
252
  }
233
253
  export function createPiShellThemeSelector(currentTheme, onSelect, onCancel, onPreview) {
234
254
  ensureTheme();
235
255
  const selector = new ThemeSelectorComponent(currentTheme, onSelect, onCancel, onPreview);
236
256
  const list = selector.getSelectList();
257
+ addPiModalSelectFooter(selector);
237
258
  adoptPiModalFrame(selector, { topIndex: 0, bottomIndex: selector.children.length - 1 });
238
259
  return componentPort(selector, data => list.handleInput(data));
239
260
  }
@@ -241,6 +262,7 @@ export function createPiShellShowImagesSelector(currentValue, onSelect, onCancel
241
262
  ensureTheme();
242
263
  const selector = new ShowImagesSelectorComponent(currentValue, onSelect, onCancel);
243
264
  const list = selector.getSelectList();
265
+ addPiModalSelectFooter(selector);
244
266
  adoptPiModalFrame(selector, { topIndex: 0, bottomIndex: selector.children.length - 1 });
245
267
  return componentPort(selector, data => list.handleInput(data));
246
268
  }
@@ -1,7 +1,40 @@
1
- import { Text } from "@earendil-works/pi-tui";
1
+ import { sliceByColumn, Text, visibleWidth } from "@earendil-works/pi-tui";
2
2
  import { formatCacheWarmingStatus } from "./shell-presenters-info.js";
3
3
  import { ensureTheme, formatSessionTokens } from "./shell-shared-facade.js";
4
4
  import { piTheme } from "./theme.js";
5
+ /** Splits a plain path by visible columns, preserving every source grapheme in order. */
6
+ function splitSessionFile(sessionFile, firstWidth, continuationWidth) {
7
+ const rows = [];
8
+ let remaining = sessionFile;
9
+ let rowWidth = firstWidth;
10
+ while (remaining) {
11
+ let row = sliceByColumn(remaining, 0, rowWidth, true);
12
+ if (!row && rowWidth !== continuationWidth) {
13
+ rows.push("");
14
+ rowWidth = continuationWidth;
15
+ continue;
16
+ }
17
+ // Compatibility: a one-column frame cannot fit a wide grapheme; preserve it for outer truncation.
18
+ if (!row)
19
+ row = sliceByColumn(remaining, 0, rowWidth);
20
+ rows.push(row);
21
+ remaining = remaining.slice(row.length);
22
+ rowWidth = continuationWidth;
23
+ }
24
+ return rows.length > 0 ? rows : [""];
25
+ }
26
+ /** Keeps the file value on its labelled row before using full continuation rows. */
27
+ function sessionFileRows(sessionFile, width) {
28
+ // Compatibility: match Text's reduced-padding rule so these explicit breaks survive its renderer.
29
+ const paddingX = Math.min(1, Math.max(0, Math.floor((width - 1) / 2)));
30
+ const contentWidth = Math.max(1, width - paddingX * 2);
31
+ const label = piTheme().fg("dim", "File:");
32
+ const firstValueWidth = Math.max(0, contentWidth - visibleWidth("File: "));
33
+ const [first = "", ...continuation] = splitSessionFile(sessionFile, firstValueWidth, contentWidth);
34
+ return firstValueWidth > 0
35
+ ? [`${label} ${first}`, ...continuation]
36
+ : [label, ...continuation];
37
+ }
5
38
  /** Renders the pinned report values while leaving group names semantic for the owned screen. */
6
39
  export function renderPiShellSessionInfoReferenceDocument(presentation, width) {
7
40
  ensureTheme();
@@ -9,7 +42,7 @@ export function renderPiShellSessionInfoReferenceDocument(presentation, width) {
9
42
  const render = (rows) => new Text(rows.join("\n"), 1, 0).render(width);
10
43
  const preamble = [
11
44
  ...(sessionName ? [`${piTheme().fg("dim", "Name:")} ${sessionName}`] : []),
12
- `${piTheme().fg("dim", "File:")} ${stats.sessionFile ?? "In-memory"}`,
45
+ ...sessionFileRows(stats.sessionFile ?? "In-memory", width),
13
46
  `${piTheme().fg("dim", "ID:")} ${stats.sessionId}`,
14
47
  ];
15
48
  const messages = [
@@ -46,7 +46,7 @@ export interface PiShellEditorPort extends PiShellComponentPort {
46
46
  keybindingConfig(): KeybindingsConfig;
47
47
  reloadKeybindings(): void;
48
48
  /** Uses Pi's terminal decoder rather than assuming one terminal escape spelling. */
49
- matchesTerminalKey(data: string, key: "home" | "end" | "ctrl+home" | "ctrl+end" | "alt+home" | "ctrl+v"): boolean;
49
+ matchesTerminalKey(data: string, key: "home" | "end" | "pageUp" | "pageDown" | "ctrl+home" | "ctrl+end" | "alt+home" | "ctrl+v"): boolean;
50
50
  getText(): string;
51
51
  setText(text: string): void;
52
52
  insertText(text: string): void;
@@ -227,6 +227,8 @@ export declare function createTuiFacade(options: Pick<PiShellEditorOptions, "get
227
227
  readonly onSubmit?: (text: string) => void;
228
228
  }): TUI;
229
229
  export declare function componentPort(component: Component, handleInput?: (data: string) => void): PiShellComponentPort;
230
+ /** Gives an extension-hosted dialog the shell's implicit close alias without rewriting its component. */
231
+ export declare function withPiDialogCancel(port: PiShellComponentPort, onCancel: () => void): PiShellComponentPort;
230
232
  export declare function componentFromPort(port: PiShellComponentPort): Component;
231
233
  export declare function formatSessionTokens(count: number): string;
232
234
  export declare function isAutocompleteProvider(value: unknown): value is AutocompleteProvider;
@@ -1,4 +1,4 @@
1
- import { hyperlink, setKeybindings, visibleWidth, truncateToWidth, } from "@earendil-works/pi-tui";
1
+ import { hyperlink, matchesKey, setKeybindings, visibleWidth, truncateToWidth, } from "@earendil-works/pi-tui";
2
2
  import { KeybindingsManager, } from "./upstream/adjacent/core/keybindings.js";
3
3
  import { ensurePiTheme, } from "./theme.js";
4
4
  /** Terminal-cell width authority used by Pi-rendered component rows. */
@@ -61,6 +61,18 @@ export function componentPort(component, handleInput) {
61
61
  ...(typeof disposable.dispose === "function" ? { dispose: () => disposable.dispose?.() } : {}),
62
62
  };
63
63
  }
64
+ /** Gives an extension-hosted dialog the shell's implicit close alias without rewriting its component. */
65
+ export function withPiDialogCancel(port, onCancel) {
66
+ return {
67
+ ...port,
68
+ handleInput: data => {
69
+ if (matchesKey(data, "ctrl+c"))
70
+ onCancel();
71
+ else
72
+ port.handleInput?.(data);
73
+ },
74
+ };
75
+ }
64
76
  export function componentFromPort(port) {
65
77
  return {
66
78
  render: width => [...port.render(width)],
@@ -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,22 @@ 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
30
  * The A1-owned searchable Skills dialog. Its composition is the model selector's (border, spacer,
19
31
  * 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
32
+ * content follows the v2 skills extension: an accent bold title, bare skill-name rows, the selected
21
33
  * skill's description cut to one line under the rows, the pinned scroll counter on overflow,
22
34
  * and the two empty states. Enter applies the selected skill with no arguments; the query is never appended.
23
35
  */
@@ -47,7 +59,7 @@ class SkillsSelectorComponent extends Container {
47
59
  this.addChild(new Text(renderPiModalShortcutHints([
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));