@timurproko/a1 0.2.7-dev.1270 → 0.2.7-dev.1275

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 (114) hide show
  1. package/README.md +6 -0
  2. package/dist/app/session-shell/apps-dialog.d.ts +33 -0
  3. package/dist/app/session-shell/apps-dialog.js +61 -0
  4. package/dist/app/session-shell/dialog-coordinator.d.ts +8 -0
  5. package/dist/app/session-shell/dialog-coordinator.js +72 -3
  6. package/dist/app/session-shell/session-selectors.d.ts +1 -1
  7. package/dist/app/session-shell/session-selectors.js +1 -1
  8. package/dist/app/session-shell/session-shell.js +9 -3
  9. package/dist/app/session-shell/shell-host.d.ts +1 -0
  10. package/dist/app/session-shell/shell-host.js +22 -11
  11. package/dist/app/session-shell/workflow-output.js +12 -0
  12. package/dist/app/session-shell/workflow-router.d.ts +1 -1
  13. package/dist/composition/owned-ui.js +17 -13
  14. package/dist/contracts/owned-ui/session-presenters.d.ts +1 -1
  15. package/dist/contracts/owned-ui/session-workflows.d.ts +3 -1
  16. package/dist/contracts/owned-ui/shell-components.d.ts +2 -0
  17. package/dist/contracts/presentation/progress-palette.generated.d.ts +1 -1
  18. package/dist/contracts/presentation/progress-palette.generated.js +1 -1
  19. package/dist/contracts/presentation/terminal.d.ts +4 -0
  20. package/dist/contracts/presentation/theme.d.ts +2 -0
  21. package/dist/foundation/lifecycle/user-exports.d.ts +5 -0
  22. package/dist/foundation/lifecycle/user-exports.js +37 -0
  23. package/dist/foundation/startup/startup-descriptor.js +2 -2
  24. package/dist/integrations/pi/components/models-dialog-loader.d.ts +11 -0
  25. package/dist/integrations/pi/components/models-dialog-loader.js +45 -0
  26. package/dist/integrations/pi/components/models-dialog.d.ts +2 -0
  27. package/dist/integrations/pi/components/models-dialog.js +13 -7
  28. package/dist/integrations/pi/components/resources/builtin-themes.d.ts +2 -2
  29. package/dist/integrations/pi/components/resources/builtin-themes.js +2 -2
  30. package/dist/integrations/pi/components/shell-editor-autocomplete.js +21 -15
  31. package/dist/integrations/pi/components/shell-footer-status.js +3 -2
  32. package/dist/integrations/pi/components/shell-presenters-info.js +1 -1
  33. package/dist/integrations/pi/components/shell-presenters-transcript.js +3 -1
  34. package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +2 -1
  35. package/dist/integrations/pi/components/shell-selectors-dialogs.js +2 -8
  36. package/dist/integrations/pi/components/theme.d.ts +2 -0
  37. package/dist/integrations/pi/components/theme.js +5 -0
  38. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +8 -7
  39. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +13 -7
  40. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +2 -2
  41. package/dist/integrations/pi/components/upstream/components/countdown-timer.d.ts +1 -1
  42. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +1 -1
  43. package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +1 -1
  44. package/dist/integrations/pi/components/upstream/components/extension-editor.js +1 -1
  45. package/dist/integrations/pi/components/upstream/components/extension-input.js +1 -1
  46. package/dist/integrations/pi/components/upstream/components/extension-selector.js +9 -2
  47. package/dist/integrations/pi/components/upstream/components/mermaid.d.ts +2 -1
  48. package/dist/integrations/pi/components/upstream/components/mermaid.js +26 -21
  49. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +1 -1
  50. package/dist/integrations/pi/components/upstream/components/owned-editor.js +1 -1
  51. package/dist/integrations/pi/components/upstream/components/pi-logo.js +1 -1
  52. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
  53. package/dist/integrations/pi/components/upstream/components/session-footer.js +9 -14
  54. package/dist/integrations/pi/components/upstream/components/session-selector-search.js +1 -1
  55. package/dist/integrations/pi/components/upstream/components/session-selector.js +1 -1
  56. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.d.ts +6 -2
  57. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.js +27 -3
  58. package/dist/integrations/pi/components/upstream/components/status-indicator.d.ts +1 -1
  59. package/dist/integrations/pi/components/upstream/components/status-indicator.js +1 -1
  60. package/dist/integrations/pi/components/upstream/components/themed-text.d.ts +1 -1
  61. package/dist/integrations/pi/components/upstream/components/themed-text.js +1 -1
  62. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +1 -1
  63. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +1 -1
  64. package/dist/integrations/pi/components/upstream/components/tool-execution.js +1 -1
  65. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +1 -1
  66. package/dist/integrations/pi/components/upstream/components/tree-selector.js +7 -6
  67. package/dist/integrations/pi/components/upstream/components/trust-selector.js +1 -1
  68. package/dist/integrations/pi/components/upstream/external-editor.js +1 -1
  69. package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +1 -1
  70. package/dist/integrations/pi/components/upstream/history/editor-core.js +2 -2
  71. package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +1 -1
  72. package/dist/integrations/pi/components/upstream/history/kill-ring.js +1 -1
  73. package/dist/integrations/pi/components/upstream/history/printable-key.js +1 -1
  74. package/dist/integrations/pi/components/upstream/history/text-helpers.js +1 -1
  75. package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +1 -1
  76. package/dist/integrations/pi/components/upstream/history/undo-stack.js +1 -1
  77. package/dist/integrations/pi/components/upstream/history/word-navigation.js +1 -1
  78. package/dist/integrations/pi/components/upstream/model-search.d.ts +1 -1
  79. package/dist/integrations/pi/components/upstream/program-status-reporter.d.ts +1 -1
  80. package/dist/integrations/pi/components/upstream/theme/theme.js +1 -1
  81. package/dist/integrations/pi/engine/adapter.d.ts +1 -1
  82. package/dist/integrations/pi/engine/adapter.js +3 -3
  83. package/dist/integrations/pi/engine/host.d.ts +1 -1
  84. package/dist/integrations/pi/engine/resources/changelog.json +4 -4
  85. package/dist/integrations/pi/engine/workflow-export.d.ts +4 -0
  86. package/dist/integrations/pi/engine/workflow-export.js +25 -0
  87. package/dist/integrations/pi/engine/workflow-runner.d.ts +1 -0
  88. package/dist/integrations/pi/engine/workflow-runner.js +6 -10
  89. package/dist/integrations/pi/engine/workflow-support.d.ts +2 -1
  90. package/dist/integrations/pi/engine/workflow-support.js +28 -25
  91. package/dist/integrations/pi/engine/workflows.d.ts +6 -1
  92. package/dist/integrations/pi/engine/workflows.js +1 -1
  93. package/dist/integrations/pi/session-presenters/presentation.js +2 -1
  94. package/dist/integrations/pi/session-presenters/presenters.js +22 -10
  95. package/dist/integrations/pi/startup-public.js +289 -240
  96. package/dist/integrations/pi/startup-public.manifest.json +579 -363
  97. package/dist/integrations/pi/tui-runtime/adapter.js +3 -1
  98. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +1 -1
  99. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +1 -1
  100. package/dist/integrations/pi/tui-runtime/overlay-geometry.d.ts +5 -2
  101. package/dist/integrations/pi/tui-runtime/overlay-geometry.js +12 -2
  102. package/dist/runtime-payload-inventory.json +8044 -6930
  103. package/dist/ui/components/dialog-panel.d.ts +4 -3
  104. package/dist/ui/components/dialog-panel.js +9 -13
  105. package/dist/ui/components/index.d.ts +1 -1
  106. package/dist/ui/components/index.js +1 -1
  107. package/dist/ui/components/spans.d.ts +7 -0
  108. package/dist/ui/components/spans.js +63 -1
  109. package/docs/architecture/prompt-suggestions.md +1 -1
  110. package/docs/architecture/ui-reference-provenance.md +2 -1
  111. package/docs/features/prompt-history.md +1 -1
  112. package/docs/features/session-exports.md +15 -0
  113. package/docs/local-worktree-cleanup.md +24 -0
  114. package/package.json +6 -6
package/README.md CHANGED
@@ -59,6 +59,12 @@ Launch A1, one agent in this terminal (profile: `~/.a1/agent`):
59
59
  a1
60
60
  ```
61
61
 
62
+ In A1, **Ctrl+P** opens the searchable **Launcher**: Agent, Settings, Changelog, and Hotkeys. You can switch from any of those screens; returning to Agent keeps the current session and draft. Settings, Changelog, and Hotkeys are applications rather than Agent slash commands.
63
+
64
+ **Ctrl+M** cycles to the next model: all available models when no scope is set, or only available scoped models when a scope exists. **`/models`** opens the in-place Models selector to select a model or manage that scope. **Ctrl+L** cycles thinking levels, and **Alt+Up** restores queued messages. In legacy terminals that encode Ctrl+M as Enter, ordinary Enter still sends the prompt; use `/models` to select a model instead. Explicit keybinding overrides remain supported. Successful model changes update the status bar without an extra success message; errors and warnings remain visible.
65
+
66
+ Session exports without an explicit destination are saved in **`~/.a1/exports/`**, outside disposable worktrees. Use `/export` for HTML or `/export --jsonl` for JSONL; explicit destinations retain their existing meaning. See [session exports](docs/features/session-exports.md).
67
+
62
68
  Resume a saved session:
63
69
 
64
70
  ```text
@@ -0,0 +1,33 @@
1
+ import type { TerminalComponentPort } from "../../contracts/presentation/index.js";
2
+ import { type UiTheme } from "../../ui/components/index.js";
3
+ declare const APPS: readonly [{
4
+ readonly id: "agent";
5
+ readonly label: "Agent";
6
+ }, {
7
+ readonly id: "settings";
8
+ readonly label: "Settings";
9
+ }, {
10
+ readonly id: "changelog";
11
+ readonly label: "Changelog";
12
+ }, {
13
+ readonly id: "hotkeys";
14
+ readonly label: "Hotkeys";
15
+ }];
16
+ type AppId = typeof APPS[number]["id"];
17
+ /** A compact application chooser; actions return to the same session and never launch a process. */
18
+ export declare class AppsDialog implements TerminalComponentPort {
19
+ #private;
20
+ readonly ports: {
21
+ theme(): UiTheme;
22
+ height(): number;
23
+ legacyKey(data: string): string;
24
+ select(id: AppId): void;
25
+ cancel(): void;
26
+ requestRender(): void;
27
+ };
28
+ constructor(ports: AppsDialog["ports"]);
29
+ render(width: number): readonly string[];
30
+ handleInput(data: string): void;
31
+ invalidate(): void;
32
+ }
33
+ export {};
@@ -0,0 +1,61 @@
1
+ import { LineInput, handleLineInputKey, ShortcutRegistry, configureDialogChoiceCapacity, renderSteppedDialogPanel } from "../../ui/components/index.js";
2
+ const APPS = [
3
+ { id: "agent", label: "Agent" },
4
+ { id: "settings", label: "Settings" },
5
+ { id: "changelog", label: "Changelog" },
6
+ { id: "hotkeys", label: "Hotkeys" },
7
+ ];
8
+ const SCOPE = "apps";
9
+ const SHORTCUTS = new ShortcutRegistry();
10
+ SHORTCUTS.declare({ key: "up", scope: SCOPE, description: "Previous app", hint: { keys: "↑↓", does: "navigate" } }, "previous");
11
+ SHORTCUTS.declare({ key: "down", scope: SCOPE, description: "Next app", hint: { keys: "↑↓", does: "navigate" } }, "next");
12
+ SHORTCUTS.declare({ key: "enter", scope: SCOPE, description: "Open app", hint: { keys: "Enter", does: "open" } }, "open");
13
+ SHORTCUTS.declare({ key: "escape", scope: SCOPE, description: "Close Launcher", hint: { keys: "Esc", does: "close" } }, "close");
14
+ /** A compact application chooser; actions return to the same session and never launch a process. */
15
+ export class AppsDialog {
16
+ #input = new LineInput();
17
+ #index = 0;
18
+ ports;
19
+ constructor(ports) { this.ports = ports; }
20
+ #matches() { return APPS.filter(app => app.label.toLowerCase().includes(this.#input.value.toLowerCase())); }
21
+ render(width) {
22
+ const matches = this.#matches();
23
+ const height = Math.max(1, this.ports.height());
24
+ const compact = height < 12;
25
+ const capacity = compact ? Math.max(1, height - 3) : configureDialogChoiceCapacity(height, true, 0);
26
+ const start = Math.max(0, this.#index - capacity + 1);
27
+ return renderSteppedDialogPanel({
28
+ title: "Launcher", input: this.#input, compact,
29
+ rows: matches.length === 0 ? [{ label: "No apps found", value: "" }]
30
+ : matches.slice(start, start + capacity).map(app => ({ label: app.label, value: "" })),
31
+ index: matches.length === 0 ? -1 : this.#index - start,
32
+ hint: [{ key: "Type", action: "search" }, ...SHORTCUTS.hintEntries(SCOPE)],
33
+ }, width, this.ports.theme()).slice(0, height);
34
+ }
35
+ handleInput(data) {
36
+ const key = this.ports.legacyKey(data);
37
+ const token = key === "\u001b[A" ? "up" : key === "\u001b[B" ? "down"
38
+ : key === "\r" || key === "\n" ? "enter" : key === "\u001b" || key === "\u0003" ? "escape" : "edit";
39
+ const action = SHORTCUTS.resolve(token, SCOPE);
40
+ const matches = this.#matches();
41
+ if (action === "close") {
42
+ this.ports.cancel();
43
+ return;
44
+ }
45
+ if (action === "open") {
46
+ const selected = matches[this.#index];
47
+ if (selected !== undefined)
48
+ this.ports.select(selected.id);
49
+ return;
50
+ }
51
+ if (action === "previous" || action === "next") {
52
+ this.#index = Math.max(0, Math.min(matches.length - 1, this.#index + (action === "next" ? 1 : -1)));
53
+ }
54
+ else {
55
+ handleLineInputKey(this.#input, key);
56
+ this.#index = 0;
57
+ }
58
+ this.ports.requestRender();
59
+ }
60
+ invalidate() { }
61
+ }
@@ -1,4 +1,5 @@
1
1
  import type { OwnedUiShellComponents, OwnedUiShellSelectorOption } from "../../contracts/owned-ui/index.js";
2
+ import type { ShellPresentationPorts } from "../../contracts/presentation/index.js";
2
3
  import type { TerminalOverlayHandle } from "../../contracts/presentation/index.js";
3
4
  import type { OwnedUiCommandResult, OwnedUiDialog } from "../../contracts/owned-ui/index.js";
4
5
  import type { UiRouteHost, UiRouteInput } from "../../ui/apps/contracts.js";
@@ -7,6 +8,10 @@ import type { OwnedUiSessionShellOptions, OwnedUiSessionShellRoot } from "./sess
7
8
  type StartupRoute = NonNullable<OwnedUiSessionShellOptions["engine"]["startupRoute"]>;
8
9
  interface SessionDialogPorts {
9
10
  readonly terminal: Pick<OwnedUiPresenterTerminal, "running" | "hasOverlay" | "showOverlay" | "openRoute" | "requestRender">;
11
+ readonly presentation?: Pick<ShellPresentationPorts, "theme" | "terminalRuntime">;
12
+ readonly viewport?: () => {
13
+ rows: number;
14
+ };
10
15
  readonly root: Pick<OwnedUiSessionShellRoot, "chrome" | "setInputSurface" | "usesDefaultInputSurface">;
11
16
  readonly routeHost: UiRouteHost | null;
12
17
  readonly dialogs: Pick<OwnedUiShellComponents["dialogs"], "dialog" | "selector">;
@@ -25,6 +30,9 @@ interface SessionDialogPorts {
25
30
  export declare class SessionDialogCoordinator {
26
31
  #private;
27
32
  constructor(ports: SessionDialogPorts);
33
+ /** Apps may cover a full-screen route, but never steal another dialog's local shortcuts. */
34
+ canOpenApps(): boolean;
35
+ showApps(): void;
28
36
  /** Clears the replacement input surface a selector put in the dock; the shared close action of every dock selector. */
29
37
  readonly closeInputSurface: () => void;
30
38
  showSelector(title: string, options: readonly OwnedUiShellSelectorOption[], onSelect: (id: string) => void, onCancel?: () => void): TerminalOverlayHandle;
@@ -10,12 +10,75 @@ export class SessionDialogCoordinator {
10
10
  #handle;
11
11
  #id;
12
12
  #source;
13
+ #route;
14
+ #apps;
13
15
  #startupRoute;
14
16
  #startupRouteTimer;
15
17
  constructor(ports) {
16
18
  this.#ports = ports;
17
19
  this.#startupRoute = ports.startupRoute;
18
20
  }
21
+ /** Apps may cover a full-screen route, but never steal another dialog's local shortcuts. */
22
+ canOpenApps() {
23
+ return this.#apps === undefined && (this.#source === "route"
24
+ || !this.#ports.terminal.hasOverlay() && this.#ports.root.usesDefaultInputSurface());
25
+ }
26
+ showApps() {
27
+ const presentation = this.#ports.presentation;
28
+ if (presentation === undefined || !this.#ports.terminal.running || !this.canOpenApps())
29
+ return;
30
+ let dialog;
31
+ let closed = false;
32
+ let loading = "Loading Launcher…";
33
+ const pending = [];
34
+ const cancel = () => { closed = true; pending.length = 0; this.#apps?.hide(); this.#apps = undefined; };
35
+ this.#apps = this.#ports.terminal.showOverlay({
36
+ render: width => dialog?.render(width) ?? [loading.slice(0, width)],
37
+ handleInput: data => {
38
+ if (dialog !== undefined)
39
+ dialog.handleInput?.(data);
40
+ else if (presentation.terminalRuntime.legacyKeyInput(data) === "\u001b")
41
+ cancel();
42
+ else if (pending.length < 32)
43
+ pending.push(data);
44
+ },
45
+ invalidate: () => dialog?.invalidate(),
46
+ dispose: () => { closed = true; pending.length = 0; },
47
+ }, { width: 60, maxHeight: "100%", anchor: "center", inputCoordination: "owned", modal: true });
48
+ // Performance: Apps and its search/list controller are optional workflows, not eager startup dependencies.
49
+ void import("./apps-dialog.js").then(({ AppsDialog }) => {
50
+ if (closed || this.#ports.disposed())
51
+ return;
52
+ dialog = new AppsDialog({
53
+ theme: () => presentation.theme.current(),
54
+ height: () => this.#ports.viewport?.().rows ?? 24,
55
+ legacyKey: data => presentation.terminalRuntime.legacyKeyInput(data),
56
+ requestRender: () => this.#ports.terminal.requestRender(),
57
+ cancel,
58
+ select: route => {
59
+ cancel();
60
+ if (route === this.#route)
61
+ return;
62
+ this.#handle?.close?.();
63
+ if (route === "agent")
64
+ this.#close();
65
+ else {
66
+ const result = this.openRoute(route);
67
+ if (result.diagnostic !== null)
68
+ this.#ports.root.chrome.setNotice({ kind: "error", message: result.diagnostic });
69
+ }
70
+ },
71
+ });
72
+ for (const key of pending.splice(0)) {
73
+ if (!closed)
74
+ dialog.handleInput?.(key);
75
+ }
76
+ this.#ports.terminal.requestRender();
77
+ }).catch(() => { if (!closed && !this.#ports.disposed()) {
78
+ loading = "Launcher unavailable — Esc close";
79
+ this.#ports.terminal.requestRender();
80
+ } });
81
+ }
19
82
  /** Clears the replacement input surface a selector put in the dock; the shared close action of every dock selector. */
20
83
  closeInputSurface = () => {
21
84
  this.#ports.root.setInputSurface(null);
@@ -29,15 +92,15 @@ export class SessionDialogCoordinator {
29
92
  title,
30
93
  options,
31
94
  onSelect: id => {
32
- onSelect(id);
33
95
  this.#close();
96
+ onSelect(id);
34
97
  },
35
98
  onCancel: () => {
36
99
  this.#close();
37
100
  onCancel?.();
38
101
  },
39
102
  });
40
- const handle = this.#ports.terminal.showOverlay(component, OVERLAY_OPTIONS);
103
+ const handle = this.#ports.terminal.showOverlay(component, { ...OVERLAY_OPTIONS, modal: true });
41
104
  this.#handle = handle;
42
105
  return handle;
43
106
  }
@@ -57,7 +120,7 @@ export class SessionDialogCoordinator {
57
120
  this.#handle?.hide();
58
121
  this.#source = "backend";
59
122
  const component = this.#ports.dialogs.dialog(dialog, { onSelect: () => this.#close(), onCancel: () => this.#close() });
60
- this.#handle = this.#ports.terminal.showOverlay(component, OVERLAY_OPTIONS);
123
+ this.#handle = this.#ports.terminal.showOverlay(component, { ...OVERLAY_OPTIONS, modal: true });
61
124
  this.#id = dialog.id;
62
125
  }
63
126
  openRoute(route, input, onClosed) {
@@ -69,6 +132,7 @@ export class SessionDialogCoordinator {
69
132
  this.#ports.root.chrome.dismissNotice();
70
133
  this.#handle?.hide();
71
134
  this.#source = "route";
135
+ this.#route = route;
72
136
  const handle = this.#ports.terminal.openRoute(surface, {
73
137
  exit: () => this.#ports.exit(),
74
138
  closed: rendered => {
@@ -118,9 +182,13 @@ export class SessionDialogCoordinator {
118
182
  if (this.#startupRouteTimer !== undefined)
119
183
  clearTimeout(this.#startupRouteTimer);
120
184
  this.#startupRouteTimer = undefined;
185
+ this.#apps?.hide();
186
+ this.#apps = undefined;
121
187
  this.#handle?.hide();
122
188
  }
123
189
  #close() {
190
+ this.#apps?.hide();
191
+ this.#apps = undefined;
124
192
  this.#handle?.hide();
125
193
  this.#clear();
126
194
  }
@@ -128,5 +196,6 @@ export class SessionDialogCoordinator {
128
196
  this.#handle = undefined;
129
197
  this.#id = undefined;
130
198
  this.#source = undefined;
199
+ this.#route = undefined;
131
200
  }
132
201
  }
@@ -42,5 +42,5 @@ export declare class SessionSelectors implements WorkflowSelectors {
42
42
  showTrustSelector(): void;
43
43
  showScopedModelsSelector(): void;
44
44
  /** The bare-A1 unified Models dialog: switch on Enter, scope on Space, persist on Ctrl+S, all through the engine. */
45
- showModelsDialog(initialQuery?: string): void;
45
+ showModelsDialog(initialQuery?: string): void | Promise<void>;
46
46
  }
@@ -220,7 +220,7 @@ export class SessionSelectors {
220
220
  }
221
221
  /** The bare-A1 unified Models dialog: switch on Enter, scope on Space, persist on Ctrl+S, all through the engine. */
222
222
  showModelsDialog(initialQuery) {
223
- this.#presenters.openModelsDialog(this.#dialogHost, initialQuery);
223
+ return this.#presenters.openModelsDialog(this.#dialogHost, initialQuery);
224
224
  }
225
225
  #treeSummary() {
226
226
  return new Promise(resolve => {
@@ -79,7 +79,7 @@ export class OwnedUiSessionPresenter {
79
79
  #sessionGeneration;
80
80
  #sessionBindingGeneration;
81
81
  constructor(host, options) {
82
- const { backend, cwd, routeHost, sessionLayout } = options.engine;
82
+ const { backend, cwd, routeHost, startupRoute, sessionLayout } = options.engine;
83
83
  const { terminal, startup, viewportSettings, reload: reloadPresentation, stream: streamPresentationOptions, input: inputPresentation, } = options.presentation ?? {};
84
84
  const { clipboard, responseCopy, paste: pasteDiagnostics, pastePreparation } = options.diagnostics ?? {};
85
85
  const promptHistory = options.history;
@@ -178,11 +178,11 @@ export class OwnedUiSessionPresenter {
178
178
  // Invariant: the presenter owns its root (theme subscription and helper clients) and disposes it once the
179
179
  // terminal no longer renders it.
180
180
  constructed.push(() => this.root.dispose());
181
- const startupRoute = options.engine.startupRoute;
182
181
  this.#dialogs = new SessionDialogCoordinator({
183
182
  terminal: this.#terminal,
184
183
  root: this.root,
185
184
  routeHost: routeHost ?? null, dialogs: options.presenters.components.dialogs,
185
+ presentation: options.presenters, viewport: () => this.#terminal.viewport(),
186
186
  disposed: () => this.#disposed,
187
187
  exit: () => { void this.shutdown(); },
188
188
  ...(startupRoute === undefined ? {} : { startupRoute }),
@@ -308,6 +308,11 @@ export class OwnedUiSessionPresenter {
308
308
  handleViewportInput(data) {
309
309
  if (!this.#inputCoordination)
310
310
  this.#streamPresentation.noteImmediatePresentation();
311
+ if (this.#customViewport && this.#dialogs.canOpenApps()
312
+ && this.root.editor.matchesAction?.(data, "owned.apps.open")) {
313
+ this.#dialogs.showApps();
314
+ return { consume: true };
315
+ }
311
316
  // Compatibility: route modal paging and boundary keys before Pi's fullscreen handlers.
312
317
  const isDefault = this.root.usesDefaultInputSurface();
313
318
  if (!this.#terminal.hasOverlay() && !isDefault
@@ -881,7 +886,8 @@ export class OwnedUiSessionPresenter {
881
886
  const separator = body.search(/\s/);
882
887
  const name = separator < 0 ? body : body.slice(0, separator);
883
888
  const argument = separator < 0 ? "" : body.slice(separator + 1).trimStart();
884
- if (this.#dialogs.claimsRoute(name))
889
+ const applicationOnly = this.#customViewport && (name === "settings" || name === "changelog" || name === "hotkeys");
890
+ if (!applicationOnly && this.#dialogs.claimsRoute(name))
885
891
  return this.#dialogs.openRoute(name);
886
892
  if (this.#isWorkflowRoute(name))
887
893
  return this.runWorkflow({ command: name, argument });
@@ -49,6 +49,7 @@ export interface OwnedUiPresenterTerminal extends OwnedUiPresenterHost {
49
49
  requestHyperlinkCleanup(rows?: readonly number[]): void;
50
50
  openRoute(surface: UiRouteSurface, callbacks: OwnedUiRouteCallbacks): {
51
51
  hide(): void;
52
+ close?(): void;
52
53
  };
53
54
  beginPresentationHold(): () => void;
54
55
  switchMode(mode: "regular" | "fullscreen"): boolean;
@@ -1,4 +1,6 @@
1
1
  import { MOUSE_TRACKING_OFF, MOUSE_TRACKING_ON, parseMouseInput } from "../../ui/components/mouse.js";
2
+ import { renderModalBackdrop } from "../../ui/components/spans.js";
3
+ import { padToWidth } from "../../ui/components/text.js";
2
4
  import { readVisibleHyperlinks } from "../../ui/components/visible-hyperlinks.js";
3
5
  import { pinnedLayoutRoot, } from "./session-shell-root.js";
4
6
  /** The size and render request a Pi component reads from the terminal it is shown on, read on every call. */
@@ -37,7 +39,7 @@ export function sessionTerminalHostOptions(options) {
37
39
  export class OwnedUiTerminalHost {
38
40
  runtime;
39
41
  #customViewport;
40
- #runtimeFactory;
42
+ #presentation;
41
43
  #damageTerminal;
42
44
  #quitOutro;
43
45
  #presenters = new Set();
@@ -51,7 +53,7 @@ export class OwnedUiTerminalHost {
51
53
  #ending;
52
54
  constructor(options) {
53
55
  this.#customViewport = options.layout === "custom-viewport";
54
- this.#runtimeFactory = options.presentation.terminalRuntime;
56
+ this.#presentation = options.presentation;
55
57
  this.#quitOutro = options.quitOutro;
56
58
  const input = options.input;
57
59
  let damageTerminal;
@@ -73,8 +75,9 @@ export class OwnedUiTerminalHost {
73
75
  mouse: false,
74
76
  consumeUnhandledMouse: true,
75
77
  onOverlayGeometry: surfaces => this.#active?.setOverlaySurfaces(surfaces),
78
+ paintModalBackdrop: renderModalBackdrop,
76
79
  decorateTerminal: (terminal) => {
77
- damageTerminal = this.#runtimeFactory.createDamageTerminal(terminal, {
80
+ damageTerminal = this.#presentation.terminalRuntime.createDamageTerminal(terminal, {
78
81
  regionalScroll: process.env.TERM !== "dumb",
79
82
  inspectHyperlinks: readVisibleHyperlinks,
80
83
  onResize: () => this.#active?.noteImmediatePresentation(),
@@ -84,7 +87,7 @@ export class OwnedUiTerminalHost {
84
87
  },
85
88
  ...(input?.coordination === false ? {} : {
86
89
  inputCoordination: {
87
- classify: (data, focusedOverlay) => this.#runtimeFactory.classifyInput(data, focusedOverlay ?? this.#active?.inputCoordinationSurface() ?? "opaque"),
90
+ classify: (data, focusedOverlay) => this.#presentation.terminalRuntime.classifyInput(data, focusedOverlay ?? this.#active?.inputCoordinationSurface() ?? "opaque"),
88
91
  onReceipt: () => this.#active?.noteImmediatePresentation(),
89
92
  ...(input?.scheduler === undefined ? {} : { scheduler: input.scheduler }),
90
93
  },
@@ -100,7 +103,7 @@ export class OwnedUiTerminalHost {
100
103
  hardwareCursor: options.hardwareCursor,
101
104
  ...(options.wheelScrollLines === undefined ? {} : { wheelScrollLines: options.wheelScrollLines }),
102
105
  };
103
- this.runtime = this.#runtimeFactory.createRuntime(runtimeOptions);
106
+ this.runtime = this.#presentation.terminalRuntime.createRuntime(runtimeOptions);
104
107
  this.#damageTerminal = damageTerminal ?? null;
105
108
  const background = this.#customViewport ? options.backgroundSettings : undefined;
106
109
  if (background !== undefined)
@@ -285,7 +288,14 @@ export class OwnedUiTerminalHost {
285
288
  #showOverlay(presenter, component, options) {
286
289
  const owned = this.#overlays.get(presenter) ?? new Set();
287
290
  this.#overlays.set(presenter, owned);
288
- const overlay = new PresenterOverlay(this.runtime.showOverlay(component, options), () => this.#active === presenter, () => owned.delete(overlay));
291
+ const overlay = new PresenterOverlay(this.runtime.showOverlay(this.#customViewport && options?.modal ? {
292
+ ...component,
293
+ render: width => component.render(width).map(line => (this.#presentation.floatingDialogSurface ?? (text => text))(`\u001b[22;39;49m${padToWidth(line, width)}`)),
294
+ invalidate: () => component.invalidate(),
295
+ handleInput: data => component.handleInput?.(data),
296
+ setFocused: focused => component.setFocused?.(focused),
297
+ dispose: () => component.dispose?.(),
298
+ } : component, options), () => this.#active === presenter, () => owned.delete(overlay));
289
299
  overlay.syncVisibility();
290
300
  owned.add(overlay);
291
301
  return overlay;
@@ -338,6 +348,7 @@ export class OwnedUiTerminalHost {
338
348
  closed = true;
339
349
  removeSurfacePreInput();
340
350
  removeInterruptWatch();
351
+ surface.close();
341
352
  this.#setPointerReporting(false);
342
353
  overlay?.hide();
343
354
  if (notify)
@@ -345,7 +356,7 @@ export class OwnedUiTerminalHost {
345
356
  };
346
357
  removeInterruptWatch = runtime.addInputListener(data => {
347
358
  // Compatibility: a kitty-keyboard child reports Ctrl+C as a CSI-u sequence rather than the byte.
348
- if (!attached() || !data.includes(INTERRUPT) && this.#runtimeFactory.legacyKeyInput(data) !== INTERRUPT)
359
+ if (!attached() || overlay?.isFocused() !== true || !data.includes(INTERRUPT) && this.#presentation.terminalRuntime.legacyKeyInput(data) !== INTERRUPT)
349
360
  return undefined;
350
361
  // Protocol: route hosts decide whether one interrupt closes their app. Raw-input forwarding
351
362
  // is required because Pi handles Ctrl+C before the fullscreen overlay receives normal input.
@@ -378,7 +389,7 @@ export class OwnedUiTerminalHost {
378
389
  // at the pre-input boundary so dropdowns, value hover, and numeric +/- work,
379
390
  // and consume even unhandled reports so settings content is never selected.
380
391
  const routeMouse = data => {
381
- if (!attached())
392
+ if (!attached() || overlay?.isFocused() !== true)
382
393
  return undefined;
383
394
  const { events, rest } = parseMouseInput(data);
384
395
  if (events.length === 0)
@@ -404,7 +415,7 @@ export class OwnedUiTerminalHost {
404
415
  for (const event of events)
405
416
  surface.handleMouse(event);
406
417
  // Invariant: owned screens see legacy key bytes whether the terminal reports legacy or kitty keys.
407
- const key = rest.length > 0 ? this.#runtimeFactory.legacyKeyInput(rest) : "";
418
+ const key = rest.length > 0 ? this.#presentation.terminalRuntime.legacyKeyInput(rest) : "";
408
419
  if (key.length > 0)
409
420
  surface.handleInput(key);
410
421
  if (surface.isClosed()) {
@@ -426,7 +437,7 @@ export class OwnedUiTerminalHost {
426
437
  anchor: "top-left",
427
438
  inputCoordination: "owned",
428
439
  });
429
- return { hide: () => closeSurface(false) };
440
+ return { hide: () => closeSurface(false), close: () => closeSurface(true) };
430
441
  }
431
442
  // Invariant: the snapshot is synchronous; the outro module itself loads only at quit.
432
443
  #captureQuitOutroFrame() {
@@ -472,7 +483,7 @@ export class OwnedUiTerminalHost {
472
483
  write: data => this.runtime.writeControl(data),
473
484
  ...(capture.settings.canvasBackgroundAnsi === null
474
485
  ? {}
475
- : { canvas: { ansi: capture.settings.canvasBackgroundAnsi, rebase: this.#runtimeFactory.rebaseDefaultBackground } }),
486
+ : { canvas: { ansi: capture.settings.canvasBackgroundAnsi, rebase: this.#presentation.terminalRuntime.rebaseDefaultBackground } }),
476
487
  ...(outro.now === undefined ? {} : { now: outro.now }),
477
488
  ...(outro.sleep === undefined ? {} : { sleep: outro.sleep }),
478
489
  ...(outro.seed === undefined ? {} : { seed: outro.seed }),
@@ -53,6 +53,18 @@ export class WorkflowOutput {
53
53
  this.#appendAnchored(width => this.#ports.components.info.commandMessage(presentation, width, this.#ports.outputPad()));
54
54
  }
55
55
  appendResult(result, errorCode) {
56
+ // Rationale: model state already repaints the footer; only actionable diagnostics need another surface.
57
+ if (this.#ports.customViewport && result.outcome === "completed"
58
+ && result.presentation?.kind === "model-change") {
59
+ for (const message of result.messages ?? []) {
60
+ if (message.kind !== "status")
61
+ this.appendMessage(message, errorCode);
62
+ }
63
+ if (result.messages === undefined && (result.messageKind === "warning" || result.messageKind === "error")) {
64
+ this.appendMessage({ kind: result.messageKind, message: result.message }, errorCode);
65
+ }
66
+ return;
67
+ }
56
68
  if (result.messages !== undefined) {
57
69
  for (const message of result.messages)
58
70
  this.appendMessage(message, errorCode);
@@ -2,7 +2,7 @@ import type { OwnedUiCommandResult, OwnedUiWorkflowRequest } from "../../contrac
2
2
  /** The owned selectors a workflow can open in place of asking the engine to choose. */
3
3
  export interface WorkflowSelectors {
4
4
  showScopedModelsSelector(): void;
5
- showModelsDialog(initialQuery?: string): void;
5
+ showModelsDialog(initialQuery?: string): void | Promise<void>;
6
6
  showModelSelector(initialSearchInput?: string): void;
7
7
  showForkSelector(): void;
8
8
  showThinkingSelector(): Promise<void>;
@@ -44,21 +44,9 @@ export async function composeOwnedUi(options = {}) {
44
44
  });
45
45
  // Invariant: bare A1 owns the settings-driven ports; the comparison profile receives none of them.
46
46
  const bareSettings = bare ? settings : null;
47
- const projectTrustPrompt = options.projectTrustPrompt;
48
47
  // Invariant: process-wide engine state has one owner. The host installs the HTTP dispatcher, applies
49
48
  // the theme singletons, and announces startup notices once; every session is created through it.
50
- const host = await (options.createEngineHost ?? (async (hostOptions) => createPiEngineHost(hostOptions)))({
51
- productMode,
52
- availableThemes: () => getAvailablePiThemes().map(theme => theme.name),
53
- announceStartupChangelog: !bare,
54
- theme: {
55
- base: bare ? "dark" : "engine-configured",
56
- accentColor: bareSettings?.value("accentColor") ?? "purple",
57
- packageBorderProjection: bare,
58
- apply: { base: applyConfiguredPiTheme, accentColor: setPiAccentColor, packageBorderProjection: setPiPackageBorderProjectionEnabled },
59
- },
60
- ...(projectTrustPrompt === undefined ? {} : { projectTrustPrompt }),
61
- });
49
+ const host = await (options.createEngineHost ?? createPiEngineHost)(engineHostOptions(options, productMode, bareSettings));
62
50
  const sessionForkPrompt = options.sessionForkPrompt;
63
51
  try {
64
52
  backend = await host.create({
@@ -247,6 +235,22 @@ function bareShellPorts(settings, generator, diagnostics) {
247
235
  },
248
236
  };
249
237
  }
238
+ function engineHostOptions(options, productMode, settings) {
239
+ const bare = productMode === "bare";
240
+ return {
241
+ productMode,
242
+ availableThemes: () => getAvailablePiThemes().map(theme => theme.name),
243
+ ...(bare ? { workflowHost: { reserveExport: async (id, format) => (await import("../foundation/lifecycle/user-exports.js")).reserveUserExport(id, format) } } : {}),
244
+ announceStartupChangelog: !bare,
245
+ theme: {
246
+ base: bare ? "dark" : "engine-configured",
247
+ accentColor: settings?.value("accentColor") ?? "purple",
248
+ packageBorderProjection: bare,
249
+ apply: { base: applyConfiguredPiTheme, accentColor: setPiAccentColor, packageBorderProjection: setPiPackageBorderProjectionEnabled },
250
+ },
251
+ ...(options.projectTrustPrompt === undefined ? {} : { projectTrustPrompt: options.projectTrustPrompt }),
252
+ };
253
+ }
250
254
  async function createPromptHistoryConfiguration(settings, backend, bare) {
251
255
  const limit = settings?.value("promptHistoryMaxItems") ?? 100;
252
256
  const scope = settings?.value("promptHistoryScope") ?? "unique";
@@ -80,6 +80,6 @@ export interface OwnedUiSessionPresenters {
80
80
  /** `navigate` completes a selection that leaves the current leaf, including any summary prompt. */
81
81
  openTreeSelector(host: OwnedUiDialogHost, navigate: (entryId: string, skipSummaryPrompt: boolean) => void, initialSelectedId?: string): Promise<void>;
82
82
  openSettingsSelector(host: OwnedUiDialogHost): void;
83
- openModelsDialog(host: OwnedUiDialogHost, initialQuery?: string): void;
83
+ openModelsDialog(host: OwnedUiDialogHost, initialQuery?: string): void | Promise<void>;
84
84
  openScopedModelsSelector(host: OwnedUiDialogHost): void;
85
85
  }
@@ -108,7 +108,9 @@ export interface OwnedUiCacheWarmingPresentation {
108
108
  };
109
109
  };
110
110
  }
111
- export type OwnedUiWorkflowPresentation = OwnedUiSessionInfoPresentation;
111
+ export type OwnedUiWorkflowPresentation = OwnedUiSessionInfoPresentation | {
112
+ readonly kind: "model-change";
113
+ };
112
114
  export type OwnedUiWorkflowMessageKind = "status" | "warning" | "error" | "accent" | "silent";
113
115
  export interface OwnedUiWorkflowMessage {
114
116
  readonly kind: Exclude<OwnedUiWorkflowMessageKind, "silent">;
@@ -53,6 +53,8 @@ export interface OwnedUiShellEditorPort extends OwnedUiShellComponentPort {
53
53
  /** Restores the shared keybindings if anything replaced the active registry; a no-op when it is already active. */
54
54
  activateKeybindings(): void;
55
55
  keybindingConfig(): OwnedUiShellKeybindings;
56
+ /** Resolves application-host actions against the same active declarations as editor dispatch. */
57
+ matchesAction?(data: string, action: "owned.apps.open"): boolean;
56
58
  reloadKeybindings(): void;
57
59
  /** Uses the vendor's terminal decoder rather than assuming one terminal escape spelling. */
58
60
  matchesTerminalKey(data: string, key: "home" | "end" | "pageUp" | "pageDown" | "ctrl+home" | "ctrl+end" | "alt+home" | "ctrl+v"): boolean;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Generated by scripts/pi/sync-progress-palette.ts from A1's named accent projection
3
- * over pinned Pi 1.5.0 (MIT), commit f9a26365568127461c47a07a37dcb42c140a3ac5. Do not edit directly.
3
+ * over pinned Pi 1.5.1 (MIT), commit dd30101cfc5b0609411dd80b6f93adbc14db14cf. Do not edit directly.
4
4
  */
5
5
  export declare const SHARED_PROGRESS_PALETTE: Readonly<{
6
6
  blue: {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Generated by scripts/pi/sync-progress-palette.ts from A1's named accent projection
3
- * over pinned Pi 1.5.0 (MIT), commit f9a26365568127461c47a07a37dcb42c140a3ac5. Do not edit directly.
3
+ * over pinned Pi 1.5.1 (MIT), commit dd30101cfc5b0609411dd80b6f93adbc14db14cf. Do not edit directly.
4
4
  */
5
5
  export const SHARED_PROGRESS_PALETTE = Object.freeze({
6
6
  "blue": {
@@ -68,6 +68,8 @@ export interface TerminalOverlayOptions {
68
68
  readonly margin?: TerminalOverlayMargin | number;
69
69
  readonly visible?: (columns: number, rows: number) => boolean;
70
70
  readonly nonCapturing?: boolean;
71
+ /** An owned floating panel; the host supplies its surface and dims the completed backdrop. */
72
+ readonly modal?: boolean;
71
73
  /** A1-owned input scheduling declaration; omitted extension overlays fail closed. */
72
74
  readonly inputCoordination?: Exclude<TerminalInputSurfaceKind, "editor">;
73
75
  }
@@ -141,6 +143,8 @@ export interface TerminalRuntimeOptions {
141
143
  readonly root: TerminalComponentPort;
142
144
  /** Bare viewport only: painted overlays in back-to-front order; null while geometry is stale. */
143
145
  readonly onOverlayGeometry?: (surfaces: readonly TerminalPointerSurface[] | null) => void;
146
+ /** Presentation policy applied after composition around the topmost owned floating panel. */
147
+ readonly paintModalBackdrop?: (lines: readonly string[], width: number, height: number, panel: TerminalPointerSurface) => string[];
144
148
  readonly mode?: "regular" | "fullscreen";
145
149
  readonly layoutRoot?: TerminalLayoutNode;
146
150
  readonly terminal?: TerminalPort;
@@ -42,5 +42,7 @@ export interface ShellPresentationPorts {
42
42
  canvasBackground(style: CanvasBackgroundStyle): string | null;
43
43
  /** Columns the pinned comparison layout pads command output with. */
44
44
  readonly pinnedOutputPad: 0 | 1;
45
+ /** Live color-capability-aware background for owned floating panels only. */
46
+ readonly floatingDialogSurface?: (text: string) => string;
45
47
  readonly terminalRuntime: TerminalRuntimeFactory;
46
48
  }
@@ -0,0 +1,5 @@
1
+ /** Exclusively reserve a default export; failure cleanup may remove only this same regular file. */
2
+ export declare function reserveUserExport(sessionId: string, format: "html" | "jsonl", home?: string): Promise<{
3
+ readonly path: string;
4
+ failed(): Promise<void>;
5
+ }>;