@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,7 +1,8 @@
1
1
  export { adaptPiAssistantMessage, adaptPiToolExecution, adaptPiUserMessage } from "./components.js";
2
2
  export { loadHistoryEditor } from "./history-editor-loader.js";
3
3
  export { PiComponentConformanceError, runPiComponentConformance } from "./conformance.js";
4
- export { OWNED_BUILTIN_SLASH_COMMANDS, PINNED_PI_BUILTIN_SLASH_COMMANDS, componentFromPort, componentPort, createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellCollapsedChangelog, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellModelsDialog, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellShowImagesSelector, createPiShellSkillsSelector, createPiShellStatus, createPiShellThemeSelector, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, createTuiFacade, ensureTheme, formatSessionTokens, isAutocompleteProvider, isPiPromptStyleCompaction, isRecord, paintPiSubmittedPromptTimestamp, piShellHyperlink, piShellTruncateToWidth, piShellVisibleWidth, renderPiShellChangelogLines, renderPiShellCommandMessage, renderPiShellHotkeysLines, renderPiShellPackageUpdateNotice, renderPiShellReleaseUpdateBanner, renderPiShellReleaseUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, validatedAssistantMessage, } from "./shell-components.js";
4
+ export { createPiShellOperationLoader, createPiShellShareOperationDialog, } from "./share-operation-dialog.js";
5
+ export { OWNED_BUILTIN_SLASH_COMMANDS, PINNED_PI_BUILTIN_SLASH_COMMANDS, componentFromPort, componentPort, createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellCollapsedChangelog, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellModelsDialog, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellShowImagesSelector, createPiShellSkillsSelector, createPiShellStatus, createPiShellThemeSelector, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, createTuiFacade, ensureTheme, formatSessionTokens, isAutocompleteProvider, isPiPromptStyleCompaction, isRecord, paintPiSubmittedPromptTimestamp, piShellHyperlink, piShellTruncateToWidth, piShellVisibleWidth, renderPiShellChangelogLines, renderPiShellCommandMessage, renderPiShellHotkeysLines, renderPiShellPackageUpdateNotice, renderPiShellReleaseUpdateBanner, renderPiShellReleaseUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, validatedAssistantMessage, } from "./shell-components.js";
5
6
  export { renderPiShellHotkeySections } from "./shell-hotkey-sections.js";
6
7
  export { PINNED_PI_LAYOUT, applyConfiguredPiTheme, applyPiTheme, applyPiThemeInstance, currentPiThemeName, detectPiTerminalBackgroundFromEnv, detectPiTerminalBackgroundTheme, detectPiTerminalThemeForAuto, ensurePiTheme, getAvailablePiThemes, loadPiTheme, onPiThemeChange, parsePiAutoThemeSetting, piTheme, resolvePiThemeSetting, stopPiThemeWatcher, } from "./theme.js";
7
8
  export { OwnedPiThemeController } from "./upstream/theme/theme-controller.js";
@@ -1,5 +1,15 @@
1
- import { Container, type Component } from "@earendil-works/pi-tui";
1
+ import { Container, type Component, type TUI } from "@earendil-works/pi-tui";
2
2
  export declare const PI_MODAL_CONTENT_PADDING_X = 1;
3
+ /** Adds the standard navigation/select/close footer before a dependency component's bottom rule. */
4
+ export declare function addPiModalSelectFooter(container: Container): void;
5
+ /** Owned cancellable operation frame with the canonical dialog close guidance. */
6
+ export declare class PiCancellableOperationFrame extends Container {
7
+ #private;
8
+ constructor(tui: TUI, message: string);
9
+ get signal(): AbortSignal;
10
+ handleInput(data: string): void;
11
+ dispose(): void;
12
+ }
3
13
  /** The semantic top rule/title pair used while a modal assembles its frame. */
4
14
  export declare class PiModalHeader implements Component {
5
15
  readonly rule: Component;
@@ -1,5 +1,33 @@
1
- import { Container, Spacer } from "@earendil-works/pi-tui";
1
+ import { CancellableLoader, Container, Spacer, Text } from "@earendil-works/pi-tui";
2
+ import { DynamicBorder } from "../startup-public.js";
3
+ import { DIALOG_CLOSE_SHORTCUT_HINT, piTheme, renderPiModalShortcutHints } from "./theme.js";
2
4
  export const PI_MODAL_CONTENT_PADDING_X = 1;
5
+ /** Adds the standard navigation/select/close footer before a dependency component's bottom rule. */
6
+ export function addPiModalSelectFooter(container) {
7
+ container.children.splice(container.children.length - 1, 0, new Text(renderPiModalShortcutHints([
8
+ { key: "↑↓", action: "navigate" }, { key: "enter", action: "select" }, DIALOG_CLOSE_SHORTCUT_HINT,
9
+ ]), 0, 0));
10
+ }
11
+ /** Owned cancellable operation frame with the canonical dialog close guidance. */
12
+ export class PiCancellableOperationFrame extends Container {
13
+ #loader;
14
+ constructor(tui, message) {
15
+ super();
16
+ const theme = piTheme();
17
+ const border = (text) => theme.fg("border", text);
18
+ this.#loader = new CancellableLoader(tui, text => theme.fg("accent", text), text => theme.fg("muted", text), message);
19
+ this.addChild(new DynamicBorder(border));
20
+ this.addChild(this.#loader);
21
+ this.addChild(new Spacer(1));
22
+ this.addChild(new Text(renderPiModalShortcutHints([DIALOG_CLOSE_SHORTCUT_HINT]), 0, 0));
23
+ this.addChild(new Spacer(1));
24
+ this.addChild(new DynamicBorder(border));
25
+ adoptPiModalFrame(this, { topIndex: 0, bottomIndex: this.children.length - 1 });
26
+ }
27
+ get signal() { return this.#loader.signal; }
28
+ handleInput(data) { this.#loader.handleInput(data); }
29
+ dispose() { this.#loader.dispose(); }
30
+ }
3
31
  /** The semantic top rule/title pair used while a modal assembles its frame. */
4
32
  export class PiModalHeader {
5
33
  rule;
@@ -1,7 +1,16 @@
1
- import { Input, Key, getKeybindings, matchesKey, truncateToWidth, } from "@earendil-works/pi-tui";
1
+ import { Input, getKeybindings, matchesKey, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
2
2
  import { DynamicBorder } from "../startup-public.js";
3
3
  import { PiModalFrame } from "./modal-frame.js";
4
- import { piTheme, renderPiModalShortcutHints } from "./theme.js";
4
+ import { DIALOG_CLOSE_SHORTCUT_HINT, piTheme, renderPiModalListRow, renderPiModalShortcutHints, } from "./theme.js";
5
+ function renderHintsWithClose(entries, width) {
6
+ const body = renderPiModalShortcutHints(entries.slice(0, -1));
7
+ const close = renderPiModalShortcutHints([DIALOG_CLOSE_SHORTCUT_HINT]);
8
+ const closeWidth = visibleWidth(close);
9
+ if (width <= closeWidth)
10
+ return truncateToWidth(close, width, "");
11
+ const clippedBody = truncateToWidth(body, Math.max(0, width - closeWidth - 2), "…");
12
+ return visibleWidth(clippedBody) === 0 ? close : `${clippedBody} ${close}`;
13
+ }
5
14
  const MAX_VISIBLE_ROWS = 10;
6
15
  const MODELS_TITLE = "Models";
7
16
  const REFRESHING_TITLE_MIN_DURATION_MS = 1_000;
@@ -166,7 +175,7 @@ export class ModelsDialogComponent {
166
175
  this.#selectedIndex = this.#selectedIndex === rows.length - 1 ? 0 : this.#selectedIndex + 1;
167
176
  return;
168
177
  }
169
- if (kb.matches(data, "tui.input.tab")) {
178
+ if (matchesKey(data, "shift+tab") || kb.matches(data, "tui.input.tab")) {
170
179
  this.#filter = this.#filter === "all" ? "scoped" : "all";
171
180
  this.#restoreSelection(preferredId);
172
181
  return;
@@ -221,15 +230,6 @@ export class ModelsDialogComponent {
221
230
  this.#setScope(next, selected.fullId);
222
231
  return;
223
232
  }
224
- if (matchesKey(data, Key.ctrl("c"))) {
225
- if (this.#input.getValue().length > 0) {
226
- this.#input.setValue("");
227
- this.#selectedIndex = 0;
228
- return;
229
- }
230
- this.#callbacks.onCancel();
231
- return;
232
- }
233
233
  if (kb.matches(data, "tui.select.cancel")) {
234
234
  this.#callbacks.onCancel();
235
235
  return;
@@ -303,13 +303,13 @@ export class ModelsDialogComponent {
303
303
  const row = rows[index];
304
304
  const selected = index === this.#selectedIndex;
305
305
  const scoped = this.#scopeIds.includes(row.fullId);
306
- // Invariant: arrow, scope marker, model id, [provider], then the active checkmark, in that order.
306
+ // Invariant: arrow, scope marker, model id, [provider], then the active checkmark.
307
307
  const prefix = selected ? theme.fg("accent", "→ ") : " ";
308
- const marker = scoped ? theme.fg("success", "●") : theme.fg("dim", "○");
309
- const label = selected ? theme.fg("accent", row.model.id) : row.model.id;
308
+ const marker = scoped ? theme.fg("accent", "●") : theme.fg("dim", "○");
309
+ const label = selected ? theme.fg("text", row.model.id) : row.model.id;
310
310
  const provider = theme.fg("muted", `[${row.model.provider}]`);
311
311
  const active = row.fullId === this.#activeModelId ? ` ${theme.fg("success", "✓")}` : "";
312
- push(`${prefix}${marker} ${label} ${provider}${active}`);
312
+ push(renderPiModalListRow(`${prefix}${marker} ${label} ${provider}${active}`, width, selected));
313
313
  }
314
314
  if (startIndex > 0 || endIndex < rows.length)
315
315
  push(theme.fg("muted", ` (${this.#selectedIndex + 1}/${rows.length})`));
@@ -322,7 +322,7 @@ export class ModelsDialogComponent {
322
322
  push();
323
323
  if (this.#refreshStatus !== undefined)
324
324
  push(theme.fg(this.#refreshStatus.kind, ` ${this.#refreshStatus.message}`));
325
- push(renderPiModalShortcutHints(this.#hints()));
325
+ push(renderHintsWithClose(this.#hints(), width));
326
326
  return lines;
327
327
  }
328
328
  #hints() {
@@ -330,13 +330,13 @@ export class ModelsDialogComponent {
330
330
  const confirm = keyLabel("tui.select.confirm");
331
331
  const save = keyLabel("app.models.save");
332
332
  return [
333
- { action: "type to search" },
333
+ { key: "Type", action: "search" },
334
334
  { key: "↑↓", action: "navigate" },
335
335
  ...(tab.length === 0 ? [] : [{ key: tab, action: "filter" }]),
336
336
  ...(confirm.length === 0 ? [] : [{ key: confirm, action: "switch" }]),
337
337
  { key: "space", action: "scope" },
338
338
  ...(save.length === 0 ? [] : [{ key: save, action: "save" }]),
339
- { key: "esc", action: "close" },
339
+ DIALOG_CLOSE_SHORTCUT_HINT,
340
340
  ];
341
341
  }
342
342
  #emptyText() {
@@ -0,0 +1,6 @@
1
+ import { type PiShellComponentPort, type PiShellEditorOptions } from "./shell-shared-facade.js";
2
+ export interface PiShellOperationLoaderPort extends PiShellComponentPort {
3
+ readonly signal: AbortSignal;
4
+ }
5
+ export declare function createPiShellShareOperationDialog(runtime: Pick<PiShellEditorOptions, "getColumns" | "getRows" | "requestRender">, message: string): PiShellOperationLoaderPort;
6
+ export declare function createPiShellOperationLoader(runtime: Pick<PiShellEditorOptions, "getColumns" | "getRows" | "requestRender">, message: string): PiShellOperationLoaderPort;
@@ -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, {
@@ -152,8 +161,12 @@ export function createPiShellEditor(options) {
152
161
  const resources = commands
153
162
  .filter(command => command.name !== collapsed.collapsedCommand?.name && command.source !== "builtin" && !builtInNames.has(command.name))
154
163
  .map(command => autocompleteCommand(command));
155
- const combined = new CombinedAutocompleteProvider([...builtIns, ...resources], options.cwd ?? process.cwd());
156
- 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;
157
170
  editor.setAutocompleteProvider(autocompleteProvider);
158
171
  };
159
172
  setAutocompleteCommands(options.autocompleteCommands ?? []);
@@ -336,6 +349,40 @@ export function createPiShellEditor(options) {
336
349
  cancelPendingPastes: () => editorUx?.cancelPendingPastes(),
337
350
  };
338
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
+ }
339
386
  function autocompleteCommand(command, addition) {
340
387
  const argumentOptions = addition?.argumentOptions ?? command.argumentOptions;
341
388
  return {
@@ -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)],