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

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 (154) 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 +62 -0
  4. package/dist/app/session-shell/dialog-coordinator.d.ts +12 -5
  5. package/dist/app/session-shell/dialog-coordinator.js +113 -15
  6. package/dist/app/session-shell/input-route.d.ts +23 -0
  7. package/dist/app/session-shell/input-route.js +59 -0
  8. package/dist/app/session-shell/session-selectors.d.ts +4 -4
  9. package/dist/app/session-shell/session-selectors.js +8 -3
  10. package/dist/app/session-shell/session-shell-root.js +6 -2
  11. package/dist/app/session-shell/session-shell.js +13 -5
  12. package/dist/app/session-shell/shell-host.d.ts +1 -0
  13. package/dist/app/session-shell/shell-host.js +22 -11
  14. package/dist/app/session-shell/viewport-pointer.js +5 -1
  15. package/dist/app/session-shell/workflow-output.js +12 -0
  16. package/dist/app/session-shell/workflow-router.d.ts +2 -2
  17. package/dist/composition/owned-ui.js +17 -13
  18. package/dist/composition/settings-route-host.js +1 -0
  19. package/dist/contracts/owned-ui/session-presenters.d.ts +1 -1
  20. package/dist/contracts/owned-ui/session-workflows.d.ts +3 -1
  21. package/dist/contracts/owned-ui/shell-components.d.ts +12 -0
  22. package/dist/contracts/presentation/component.d.ts +7 -0
  23. package/dist/contracts/presentation/index.d.ts +2 -0
  24. package/dist/contracts/presentation/index.js +4 -0
  25. package/dist/contracts/presentation/progress-palette.generated.d.ts +25 -1
  26. package/dist/contracts/presentation/progress-palette.generated.js +2 -135
  27. package/dist/contracts/presentation/terminal.d.ts +4 -0
  28. package/dist/contracts/presentation/theme.d.ts +2 -0
  29. package/dist/features/owned-ui/project-trust-dialog.d.ts +1 -1
  30. package/dist/features/owned-ui/project-trust-dialog.js +5 -5
  31. package/dist/features/owned-ui/project-trust-prompt.js +2 -2
  32. package/dist/features/owned-ui/reference-screen-app.d.ts +2 -0
  33. package/dist/features/owned-ui/reference-screen-app.js +4 -1
  34. package/dist/features/owned-ui/settings-app.js +6 -6
  35. package/dist/features/owned-ui/settings-model-dialog.js +3 -3
  36. package/dist/foundation/lifecycle/user-exports.d.ts +5 -0
  37. package/dist/foundation/lifecycle/user-exports.js +37 -0
  38. package/dist/foundation/startup/startup-descriptor.js +2 -2
  39. package/dist/integrations/pi/components/deferred-selector.d.ts +6 -0
  40. package/dist/integrations/pi/components/deferred-selector.js +40 -0
  41. package/dist/integrations/pi/components/login-dialog-loader.d.ts +3 -0
  42. package/dist/integrations/pi/components/login-dialog-loader.js +57 -0
  43. package/dist/integrations/pi/components/login-links.d.ts +6 -0
  44. package/dist/integrations/pi/components/login-links.js +31 -0
  45. package/dist/integrations/pi/components/login-text.d.ts +7 -0
  46. package/dist/integrations/pi/components/login-text.js +17 -0
  47. package/dist/integrations/pi/components/modal-frame.d.ts +5 -0
  48. package/dist/integrations/pi/components/modal-frame.js +30 -2
  49. package/dist/integrations/pi/components/models-dialog-loader.d.ts +11 -0
  50. package/dist/integrations/pi/components/models-dialog-loader.js +45 -0
  51. package/dist/integrations/pi/components/models-dialog.d.ts +2 -0
  52. package/dist/integrations/pi/components/models-dialog.js +13 -7
  53. package/dist/integrations/pi/components/resources/builtin-themes.d.ts +2 -2
  54. package/dist/integrations/pi/components/resources/builtin-themes.js +2 -2
  55. package/dist/integrations/pi/components/shell-editor-autocomplete.js +21 -15
  56. package/dist/integrations/pi/components/shell-footer-status.js +3 -2
  57. package/dist/integrations/pi/components/shell-presenters-info.js +1 -1
  58. package/dist/integrations/pi/components/shell-presenters-transcript.js +3 -1
  59. package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +13 -9
  60. package/dist/integrations/pi/components/shell-selectors-dialogs.js +38 -43
  61. package/dist/integrations/pi/components/shell-session-info-reference.d.ts +1 -1
  62. package/dist/integrations/pi/components/shell-session-info-reference.js +49 -46
  63. package/dist/integrations/pi/components/theme.d.ts +4 -0
  64. package/dist/integrations/pi/components/theme.js +9 -0
  65. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +8 -7
  66. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +13 -7
  67. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +2 -2
  68. package/dist/integrations/pi/components/upstream/components/auth-url.d.ts +16 -0
  69. package/dist/integrations/pi/components/upstream/components/auth-url.js +49 -0
  70. package/dist/integrations/pi/components/upstream/components/countdown-timer.d.ts +1 -1
  71. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +1 -1
  72. package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +1 -1
  73. package/dist/integrations/pi/components/upstream/components/extension-editor.js +1 -1
  74. package/dist/integrations/pi/components/upstream/components/extension-input.js +1 -1
  75. package/dist/integrations/pi/components/upstream/components/extension-selector.d.ts +1 -0
  76. package/dist/integrations/pi/components/upstream/components/extension-selector.js +30 -12
  77. package/dist/integrations/pi/components/upstream/components/login-dialog.d.ts +3 -0
  78. package/dist/integrations/pi/components/upstream/components/login-dialog.js +216 -0
  79. package/dist/integrations/pi/components/upstream/components/mermaid.d.ts +2 -1
  80. package/dist/integrations/pi/components/upstream/components/mermaid.js +26 -21
  81. package/dist/integrations/pi/components/upstream/components/oauth-selector.d.ts +19 -0
  82. package/dist/integrations/pi/components/upstream/components/oauth-selector.js +123 -0
  83. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +1 -1
  84. package/dist/integrations/pi/components/upstream/components/owned-editor.js +1 -1
  85. package/dist/integrations/pi/components/upstream/components/pi-logo.js +1 -1
  86. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
  87. package/dist/integrations/pi/components/upstream/components/session-footer.js +17 -19
  88. package/dist/integrations/pi/components/upstream/components/session-selector-search.js +1 -1
  89. package/dist/integrations/pi/components/upstream/components/session-selector.js +1 -1
  90. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.d.ts +6 -2
  91. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.js +27 -3
  92. package/dist/integrations/pi/components/upstream/components/status-indicator.d.ts +1 -1
  93. package/dist/integrations/pi/components/upstream/components/status-indicator.js +1 -1
  94. package/dist/integrations/pi/components/upstream/components/themed-text.d.ts +1 -1
  95. package/dist/integrations/pi/components/upstream/components/themed-text.js +1 -1
  96. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +1 -1
  97. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +1 -1
  98. package/dist/integrations/pi/components/upstream/components/tool-execution.js +1 -1
  99. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +1 -1
  100. package/dist/integrations/pi/components/upstream/components/tree-selector.js +7 -6
  101. package/dist/integrations/pi/components/upstream/components/trust-selector.d.ts +2 -0
  102. package/dist/integrations/pi/components/upstream/components/trust-selector.js +30 -13
  103. package/dist/integrations/pi/components/upstream/external-editor.js +1 -1
  104. package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +1 -1
  105. package/dist/integrations/pi/components/upstream/history/editor-core.js +2 -2
  106. package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +1 -1
  107. package/dist/integrations/pi/components/upstream/history/kill-ring.js +1 -1
  108. package/dist/integrations/pi/components/upstream/history/printable-key.js +1 -1
  109. package/dist/integrations/pi/components/upstream/history/text-helpers.js +1 -1
  110. package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +1 -1
  111. package/dist/integrations/pi/components/upstream/history/undo-stack.js +1 -1
  112. package/dist/integrations/pi/components/upstream/history/word-navigation.js +1 -1
  113. package/dist/integrations/pi/components/upstream/model-search.d.ts +1 -1
  114. package/dist/integrations/pi/components/upstream/program-status-reporter.d.ts +1 -1
  115. package/dist/integrations/pi/components/upstream/theme/theme.js +1 -1
  116. package/dist/integrations/pi/engine/adapter.d.ts +1 -1
  117. package/dist/integrations/pi/engine/adapter.js +3 -3
  118. package/dist/integrations/pi/engine/host.d.ts +1 -1
  119. package/dist/integrations/pi/engine/resources/changelog.json +4 -4
  120. package/dist/integrations/pi/engine/workflow-export.d.ts +4 -0
  121. package/dist/integrations/pi/engine/workflow-export.js +25 -0
  122. package/dist/integrations/pi/engine/workflow-runner.d.ts +1 -0
  123. package/dist/integrations/pi/engine/workflow-runner.js +6 -10
  124. package/dist/integrations/pi/engine/workflow-support.d.ts +2 -1
  125. package/dist/integrations/pi/engine/workflow-support.js +28 -25
  126. package/dist/integrations/pi/engine/workflows.d.ts +6 -1
  127. package/dist/integrations/pi/engine/workflows.js +1 -1
  128. package/dist/integrations/pi/session-presenters/components.d.ts +1 -1
  129. package/dist/integrations/pi/session-presenters/components.js +8 -8
  130. package/dist/integrations/pi/session-presenters/presentation.js +2 -1
  131. package/dist/integrations/pi/session-presenters/presenters.js +24 -12
  132. package/dist/integrations/pi/startup-public.js +289 -240
  133. package/dist/integrations/pi/startup-public.manifest.json +579 -363
  134. package/dist/integrations/pi/tui-runtime/adapter.js +3 -1
  135. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +1 -1
  136. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +1 -1
  137. package/dist/integrations/pi/tui-runtime/overlay-geometry.d.ts +5 -2
  138. package/dist/integrations/pi/tui-runtime/overlay-geometry.js +12 -2
  139. package/dist/runtime-payload-inventory.json +8044 -6930
  140. package/dist/ui/components/dialog-panel.d.ts +4 -3
  141. package/dist/ui/components/dialog-panel.js +9 -13
  142. package/dist/ui/components/index.d.ts +1 -1
  143. package/dist/ui/components/index.js +1 -1
  144. package/dist/ui/components/list-block.d.ts +2 -2
  145. package/dist/ui/components/list-block.js +4 -3
  146. package/dist/ui/components/spans.d.ts +7 -0
  147. package/dist/ui/components/spans.js +63 -1
  148. package/dist/ui/settings/early-accent.d.ts +24 -0
  149. package/docs/architecture/prompt-suggestions.md +1 -1
  150. package/docs/architecture/ui-reference-provenance.md +2 -1
  151. package/docs/features/prompt-history.md +1 -1
  152. package/docs/features/session-exports.md +15 -0
  153. package/docs/local-worktree-cleanup.md +24 -0
  154. 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,62 @@
1
+ import { wrapChoiceIndex } from "../../contracts/presentation/index.js";
2
+ import { LineInput, handleLineInputKey, ShortcutRegistry, configureDialogChoiceCapacity, renderSteppedDialogPanel } from "../../ui/components/index.js";
3
+ const APPS = [
4
+ { id: "agent", label: "Agent" },
5
+ { id: "settings", label: "Settings" },
6
+ { id: "changelog", label: "Changelog" },
7
+ { id: "hotkeys", label: "Hotkeys" },
8
+ ];
9
+ const SCOPE = "apps";
10
+ const SHORTCUTS = new ShortcutRegistry();
11
+ SHORTCUTS.declare({ key: "up", scope: SCOPE, description: "Previous app", hint: { keys: "↑↓", does: "navigate" } }, "previous");
12
+ SHORTCUTS.declare({ key: "down", scope: SCOPE, description: "Next app", hint: { keys: "↑↓", does: "navigate" } }, "next");
13
+ SHORTCUTS.declare({ key: "enter", scope: SCOPE, description: "Open app", hint: { keys: "Enter", does: "open" } }, "open");
14
+ SHORTCUTS.declare({ key: "escape", scope: SCOPE, description: "Close Launcher", hint: { keys: "Esc", does: "close" } }, "close");
15
+ /** A compact application chooser; actions return to the same session and never launch a process. */
16
+ export class AppsDialog {
17
+ #input = new LineInput();
18
+ #index = 0;
19
+ ports;
20
+ constructor(ports) { this.ports = ports; }
21
+ #matches() { return APPS.filter(app => app.label.toLowerCase().includes(this.#input.value.toLowerCase())); }
22
+ render(width) {
23
+ const matches = this.#matches();
24
+ const height = Math.max(1, this.ports.height());
25
+ const compact = height < 12;
26
+ const capacity = compact ? Math.max(1, height - 3) : configureDialogChoiceCapacity(height, true, 0);
27
+ const start = Math.max(0, this.#index - capacity + 1);
28
+ return renderSteppedDialogPanel({
29
+ title: "Launcher", input: this.#input, compact,
30
+ rows: matches.length === 0 ? [{ label: "No apps found", value: "" }]
31
+ : matches.slice(start, start + capacity).map(app => ({ label: app.label, value: "" })),
32
+ index: matches.length === 0 ? -1 : this.#index - start,
33
+ hint: [{ key: "Type", action: "search" }, ...SHORTCUTS.hintEntries(SCOPE)],
34
+ }, width, this.ports.theme()).slice(0, height);
35
+ }
36
+ handleInput(data) {
37
+ const key = this.ports.legacyKey(data);
38
+ const token = key === "\u001b[A" ? "up" : key === "\u001b[B" ? "down"
39
+ : key === "\r" || key === "\n" ? "enter" : key === "\u001b" || key === "\u0003" ? "escape" : "edit";
40
+ const action = SHORTCUTS.resolve(token, SCOPE);
41
+ const matches = this.#matches();
42
+ if (action === "close") {
43
+ this.ports.cancel();
44
+ return;
45
+ }
46
+ if (action === "open") {
47
+ const selected = matches[this.#index];
48
+ if (selected !== undefined)
49
+ this.ports.select(selected.id);
50
+ return;
51
+ }
52
+ if (action === "previous" || action === "next") {
53
+ this.#index = wrapChoiceIndex(this.#index, action === "next" ? 1 : -1, matches.length);
54
+ }
55
+ else {
56
+ handleLineInputKey(this.#input, key);
57
+ this.#index = 0;
58
+ }
59
+ this.ports.requestRender();
60
+ }
61
+ invalidate() { }
62
+ }
@@ -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,11 @@ 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
+ };
15
+ readonly inPlaceSession?: boolean;
10
16
  readonly root: Pick<OwnedUiSessionShellRoot, "chrome" | "setInputSurface" | "usesDefaultInputSurface">;
11
17
  readonly routeHost: UiRouteHost | null;
12
18
  readonly dialogs: Pick<OwnedUiShellComponents["dialogs"], "dialog" | "selector">;
@@ -17,20 +23,21 @@ interface SessionDialogPorts {
17
23
  readonly routeClosed?: (route: string) => void;
18
24
  readonly startupRoute?: StartupRoute;
19
25
  }
20
- /**
21
- * Owns the one modal overlay a session shows at a time, whichever source opened it: an owned route,
22
- * a local selector, or a backend dialog. Opening one hides the previous one, and every close path
23
- * clears the same state.
24
- */
26
+ /** Coordinates owned routes, dock dialogs, and backend overlays through one replacement/close lifecycle. */
25
27
  export declare class SessionDialogCoordinator {
26
28
  #private;
27
29
  constructor(ports: SessionDialogPorts);
30
+ /** Apps may cover a full-screen route, but never steal another dialog's local shortcuts. */
31
+ canOpenApps(): boolean;
32
+ showApps(): void;
28
33
  /** Clears the replacement input surface a selector put in the dock; the shared close action of every dock selector. */
29
34
  readonly closeInputSurface: () => void;
30
35
  showSelector(title: string, options: readonly OwnedUiShellSelectorOption[], onSelect: (id: string) => void, onCancel?: () => void): TerminalOverlayHandle;
31
36
  /** Mirrors the backend's pending dialog; locally owned routes and selectors are left alone when it has none. */
32
37
  syncBackend(dialog: OwnedUiDialog | null): void;
33
38
  openRoute(route: string, input?: UiRouteInput, onClosed?: () => void | Promise<void>): OwnedUiCommandResult;
39
+ /** Close the route before global abort. */
40
+ handleInputRouteInterrupt(data: string): boolean;
34
41
  claimsRoute(name: string): boolean;
35
42
  /** Retains a route requested before the runtime runs; the latest request wins. */
36
43
  retainStartupRoute(route: string): void;
@@ -1,21 +1,82 @@
1
+ import { InputRoute } from "./input-route.js";
1
2
  const OVERLAY_OPTIONS = { width: "70%", maxHeight: "80%", anchor: "center", inputCoordination: "owned" };
2
3
  const STARTUP_ROUTE_RETRY_MS = 25;
3
- /**
4
- * Owns the one modal overlay a session shows at a time, whichever source opened it: an owned route,
5
- * a local selector, or a backend dialog. Opening one hides the previous one, and every close path
6
- * clears the same state.
7
- */
4
+ /** Coordinates owned routes, dock dialogs, and backend overlays through one replacement/close lifecycle. */
8
5
  export class SessionDialogCoordinator {
9
6
  #ports;
10
7
  #handle;
11
8
  #id;
12
9
  #source;
10
+ #route;
11
+ #apps;
12
+ #inputRoute;
13
13
  #startupRoute;
14
14
  #startupRouteTimer;
15
15
  constructor(ports) {
16
16
  this.#ports = ports;
17
17
  this.#startupRoute = ports.startupRoute;
18
18
  }
19
+ /** Apps may cover a full-screen route, but never steal another dialog's local shortcuts. */
20
+ canOpenApps() {
21
+ return this.#apps === undefined && (this.#source === "route"
22
+ || !this.#ports.terminal.hasOverlay() && this.#ports.root.usesDefaultInputSurface());
23
+ }
24
+ showApps() {
25
+ const presentation = this.#ports.presentation;
26
+ if (presentation === undefined || !this.#ports.terminal.running || !this.canOpenApps())
27
+ return;
28
+ let dialog;
29
+ let closed = false;
30
+ let loading = "Loading Launcher…";
31
+ const pending = [];
32
+ const cancel = () => { closed = true; pending.length = 0; this.#apps?.hide(); this.#apps = undefined; };
33
+ this.#apps = this.#ports.terminal.showOverlay({
34
+ render: width => dialog?.render(width) ?? [loading.slice(0, width)],
35
+ handleInput: data => {
36
+ if (dialog !== undefined)
37
+ dialog.handleInput?.(data);
38
+ else if (presentation.terminalRuntime.legacyKeyInput(data) === "\u001b")
39
+ cancel();
40
+ else if (pending.length < 32)
41
+ pending.push(data);
42
+ },
43
+ invalidate: () => dialog?.invalidate(),
44
+ dispose: () => { closed = true; pending.length = 0; },
45
+ }, { width: 60, maxHeight: "100%", anchor: "center", inputCoordination: "owned", modal: true });
46
+ // Performance: Apps and its search/list controller are optional workflows, not eager startup dependencies.
47
+ void import("./apps-dialog.js").then(({ AppsDialog }) => {
48
+ if (closed || this.#ports.disposed())
49
+ return;
50
+ dialog = new AppsDialog({
51
+ theme: () => presentation.theme.current(),
52
+ height: () => this.#ports.viewport?.().rows ?? 24,
53
+ legacyKey: data => presentation.terminalRuntime.legacyKeyInput(data),
54
+ requestRender: () => this.#ports.terminal.requestRender(),
55
+ cancel,
56
+ select: route => {
57
+ cancel();
58
+ if (route === this.#route)
59
+ return;
60
+ this.#handle?.close?.();
61
+ if (route === "agent")
62
+ this.#close();
63
+ else {
64
+ const result = this.openRoute(route);
65
+ if (result.diagnostic !== null)
66
+ this.#ports.root.chrome.setNotice({ kind: "error", message: result.diagnostic });
67
+ }
68
+ },
69
+ });
70
+ for (const key of pending.splice(0)) {
71
+ if (!closed)
72
+ dialog.handleInput?.(key);
73
+ }
74
+ this.#ports.terminal.requestRender();
75
+ }).catch(() => { if (!closed && !this.#ports.disposed()) {
76
+ loading = "Launcher unavailable — Esc close";
77
+ this.#ports.terminal.requestRender();
78
+ } });
79
+ }
19
80
  /** Clears the replacement input surface a selector put in the dock; the shared close action of every dock selector. */
20
81
  closeInputSurface = () => {
21
82
  this.#ports.root.setInputSurface(null);
@@ -29,15 +90,15 @@ export class SessionDialogCoordinator {
29
90
  title,
30
91
  options,
31
92
  onSelect: id => {
32
- onSelect(id);
33
93
  this.#close();
94
+ onSelect(id);
34
95
  },
35
96
  onCancel: () => {
36
97
  this.#close();
37
98
  onCancel?.();
38
99
  },
39
100
  });
40
- const handle = this.#ports.terminal.showOverlay(component, OVERLAY_OPTIONS);
101
+ const handle = this.#ports.terminal.showOverlay(component, { ...OVERLAY_OPTIONS, modal: true });
41
102
  this.#handle = handle;
42
103
  return handle;
43
104
  }
@@ -57,7 +118,7 @@ export class SessionDialogCoordinator {
57
118
  this.#handle?.hide();
58
119
  this.#source = "backend";
59
120
  const component = this.#ports.dialogs.dialog(dialog, { onSelect: () => this.#close(), onCancel: () => this.#close() });
60
- this.#handle = this.#ports.terminal.showOverlay(component, OVERLAY_OPTIONS);
121
+ this.#handle = this.#ports.terminal.showOverlay(component, { ...OVERLAY_OPTIONS, modal: true });
61
122
  this.#id = dialog.id;
62
123
  }
63
124
  openRoute(route, input, onClosed) {
@@ -69,24 +130,55 @@ export class SessionDialogCoordinator {
69
130
  this.#ports.root.chrome.dismissNotice();
70
131
  this.#handle?.hide();
71
132
  this.#source = "route";
133
+ this.#route = route;
134
+ const closed = (rendered) => {
135
+ if (rendered) {
136
+ try {
137
+ void onClosed?.()?.catch(() => undefined);
138
+ }
139
+ catch { /* Rationale: a failed acknowledgement stays pending. */ }
140
+ this.#ports.routeClosed?.(route);
141
+ }
142
+ };
143
+ if (route === "session" && this.#ports.inPlaceSession) {
144
+ const component = new InputRoute(surface, {
145
+ requestRender: () => this.#ports.terminal.requestRender(),
146
+ closed: rendered => { this.closeInputSurface(); closed(rendered); },
147
+ released: () => { if (this.#handle === handle)
148
+ this.#clear(); },
149
+ key: data => this.#ports.presentation?.terminalRuntime.legacyKeyInput(data) ?? data,
150
+ exit: () => this.#ports.exit(),
151
+ });
152
+ const handle = { hide: () => component.close(), close: () => component.close() };
153
+ this.#handle = handle;
154
+ this.#inputRoute = component;
155
+ this.#id = surface.id;
156
+ this.#ports.root.setInputSurface(component);
157
+ this.#ports.terminal.requestRender();
158
+ return { outcome: "completed", diagnostic: null };
159
+ }
72
160
  const handle = this.#ports.terminal.openRoute(surface, {
73
161
  exit: () => this.#ports.exit(),
74
162
  closed: rendered => {
75
163
  if (this.#handle === handle)
76
164
  this.#clear();
77
- if (rendered) {
78
- try {
79
- void onClosed?.()?.catch(() => undefined);
80
- }
81
- catch { /* Rationale: a failed acknowledgement stays pending. */ }
82
- this.#ports.routeClosed?.(route);
83
- }
165
+ closed(rendered);
84
166
  },
85
167
  });
86
168
  this.#handle = handle;
87
169
  this.#id = surface.id;
88
170
  return { outcome: "completed", diagnostic: null };
89
171
  }
172
+ /** Close the route before global abort. */
173
+ handleInputRouteInterrupt(data) {
174
+ if (this.#inputRoute === undefined || this.#apps !== undefined)
175
+ return false;
176
+ const key = this.#ports.presentation?.terminalRuntime.legacyKeyInput(data) ?? data;
177
+ if (key !== "\u0003")
178
+ return false;
179
+ this.#inputRoute.handleInput(key);
180
+ return true;
181
+ }
90
182
  claimsRoute(name) {
91
183
  return this.#ports.routeHost?.claims(name) ?? false;
92
184
  }
@@ -118,9 +210,13 @@ export class SessionDialogCoordinator {
118
210
  if (this.#startupRouteTimer !== undefined)
119
211
  clearTimeout(this.#startupRouteTimer);
120
212
  this.#startupRouteTimer = undefined;
213
+ this.#apps?.hide();
214
+ this.#apps = undefined;
121
215
  this.#handle?.hide();
122
216
  }
123
217
  #close() {
218
+ this.#apps?.hide();
219
+ this.#apps = undefined;
124
220
  this.#handle?.hide();
125
221
  this.#clear();
126
222
  }
@@ -128,5 +224,7 @@ export class SessionDialogCoordinator {
128
224
  this.#handle = undefined;
129
225
  this.#id = undefined;
130
226
  this.#source = undefined;
227
+ this.#route = undefined;
228
+ this.#inputRoute = undefined;
131
229
  }
132
230
  }
@@ -0,0 +1,23 @@
1
+ import type { OwnedUiShellComponentPort } from "../../contracts/owned-ui/index.js";
2
+ import type { UiRouteSurface } from "../../ui/apps/index.js";
3
+ import type { PaneMouseEvent } from "../../ui/components/index.js";
4
+ type InputRouteOptions = {
5
+ readonly requestRender: () => void;
6
+ readonly closed: (rendered: boolean) => void;
7
+ readonly exit: () => void;
8
+ readonly released: () => void;
9
+ readonly key: (data: string) => string;
10
+ };
11
+ /** Hosts a bounded dock route. */
12
+ export declare class InputRoute implements OwnedUiShellComponentPort {
13
+ #private;
14
+ constructor(surface: UiRouteSurface, options: InputRouteOptions);
15
+ render(width: number): readonly string[];
16
+ renderInDock(width: number, maxHeight: number): readonly string[];
17
+ handleInput(data: string): void;
18
+ handleLocalMouse(event: PaneMouseEvent): void;
19
+ invalidate(): void;
20
+ close(): void;
21
+ dispose(): void;
22
+ }
23
+ export {};
@@ -0,0 +1,59 @@
1
+ /** Hosts a bounded dock route. */
2
+ export class InputRoute {
3
+ #surface;
4
+ #options;
5
+ #disposed = false;
6
+ #rendered = false;
7
+ constructor(surface, options) {
8
+ this.#surface = surface;
9
+ this.#options = options;
10
+ surface.onRenderRequested(() => { if (!this.#disposed)
11
+ this.#refresh(); });
12
+ surface.onExitRequested(() => { this.close(); options.exit(); });
13
+ }
14
+ render(width) { return this.renderInDock(width, 24); }
15
+ renderInDock(width, maxHeight) {
16
+ if (this.#disposed)
17
+ return [];
18
+ const frame = this.#surface.render(width, Math.max(1, Math.min(24, maxHeight)));
19
+ this.#rendered = true;
20
+ return frame;
21
+ }
22
+ handleInput(data) {
23
+ if (this.#disposed)
24
+ return;
25
+ const key = this.#options.key(data);
26
+ // Compatibility: close even while the deferred document is loading.
27
+ if (key === "\u001b" || key === "\u0003") {
28
+ this.close();
29
+ return;
30
+ }
31
+ this.#surface.handleInput(key);
32
+ this.#refresh();
33
+ }
34
+ handleLocalMouse(event) {
35
+ if (this.#disposed)
36
+ return;
37
+ this.#surface.handleMouse(event);
38
+ this.#refresh();
39
+ }
40
+ invalidate() { }
41
+ close() {
42
+ if (this.#disposed)
43
+ return;
44
+ this.#options.closed(this.#rendered);
45
+ this.dispose();
46
+ }
47
+ dispose() {
48
+ if (this.#disposed)
49
+ return;
50
+ this.#disposed = true;
51
+ this.#surface.close();
52
+ this.#options.released();
53
+ }
54
+ #refresh() {
55
+ if (this.#surface.isClosed())
56
+ this.close();
57
+ this.#options.requestRender();
58
+ }
59
+ }
@@ -28,8 +28,8 @@ export declare class SessionSelectors implements WorkflowSelectors {
28
28
  #private;
29
29
  constructor(ports: SessionSelectorPorts);
30
30
  showThinkingSelector(): Promise<void>;
31
- /** The Skills dialog: a replacement input like the model selector, applying the chosen skill through the prompt path. */
32
- showSkillsSelector(skills: readonly OwnedUiShellSkillSummary[]): void;
31
+ /** Applies a dock-selected skill through the prompt path. */
32
+ showSkillsSelector(skills: readonly OwnedUiShellSkillSummary[]): Promise<void> | undefined;
33
33
  showModelSelector(initialSearchInput?: string): void;
34
34
  showForkSelector(): void;
35
35
  showLogoutSelector(): Promise<void>;
@@ -39,8 +39,8 @@ export declare class SessionSelectors implements WorkflowSelectors {
39
39
  showLoginProviderSelector(authType?: "oauth" | "api_key", initialSearchInput?: string): void;
40
40
  showSessionSelector(): Promise<void>;
41
41
  showSettingsSelector(): void;
42
- showTrustSelector(): void;
42
+ showTrustSelector(): Promise<void> | undefined;
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
  }
@@ -77,7 +77,7 @@ export class SessionSelectors {
77
77
  releasePresentation?.();
78
78
  }
79
79
  }
80
- /** The Skills dialog: a replacement input like the model selector, applying the chosen skill through the prompt path. */
80
+ /** Applies a dock-selected skill through the prompt path. */
81
81
  showSkillsSelector(skills) {
82
82
  const close = this.#dialogs.closeInputSurface;
83
83
  const component = this.#presenters.components.dialogs.skillsSelector({
@@ -89,6 +89,7 @@ export class SessionSelectors {
89
89
  onCancel: close,
90
90
  });
91
91
  this.#present(component);
92
+ return component.ready;
92
93
  }
93
94
  showModelSelector(initialSearchInput) {
94
95
  this.#presenters.openModelSelector(this.#dialogHost, initialSearchInput);
@@ -214,13 +215,14 @@ export class SessionSelectors {
214
215
  onCancel: close,
215
216
  });
216
217
  this.#present(component);
218
+ return component.ready;
217
219
  }
218
220
  showScopedModelsSelector() {
219
221
  this.#presenters.openScopedModelsSelector(this.#dialogHost);
220
222
  }
221
223
  /** The bare-A1 unified Models dialog: switch on Enter, scope on Space, persist on Ctrl+S, all through the engine. */
222
224
  showModelsDialog(initialQuery) {
223
- this.#presenters.openModelsDialog(this.#dialogHost, initialQuery);
225
+ return this.#presenters.openModelsDialog(this.#dialogHost, initialQuery);
224
226
  }
225
227
  #treeSummary() {
226
228
  return new Promise(resolve => {
@@ -261,9 +263,12 @@ export class SessionSelectors {
261
263
  if (result.diagnostic === "Branch summarization cancelled")
262
264
  await this.showTreeSelector(entryId);
263
265
  }
264
- // Invariant: a selector replaces the editor in the input dock and paints in the same frame.
266
+ // Performance: retain the frame until a deferred selector is ready.
265
267
  #present(component) {
268
+ const release = component?.ready === undefined ? undefined : this.#terminal.beginPresentationHold();
266
269
  this.#root.setInputSurface(component);
270
+ void component?.ready?.finally(() => { release?.(); if (!this.#ports.disposed())
271
+ this.#terminal.requestRender(); });
267
272
  this.#terminal.requestRender();
268
273
  }
269
274
  }
@@ -357,10 +357,14 @@ export class OwnedUiSessionShellRoot {
357
357
  const messageStart = queued.length;
358
358
  const copyableMessageRows = Array.from({ length: statusRows.length + noticeRows.length }, (_value, index) => ({ row: messageStart + index }));
359
359
  const aboveWidgets = this.#extensions.renderWidgets("aboveEditor", width);
360
- const input = this.#input.surface.render(width);
361
- const body = this.usesDefaultInputSurface() ? this.editor.bodyGeometry?.() : undefined;
362
360
  const belowWidgets = this.#extensions.renderWidgets("belowEditor", width);
363
361
  const footer = this.#renderFooter(width);
362
+ // Invariant: reserve transcript context above scrollable routes.
363
+ const available = Math.max(1, this.#componentRuntime.getRows() - transientRows.length - aboveWidgets.length - belowWidgets.length - footer.length - 3);
364
+ const surface = this.#input.surface;
365
+ const input = this.#customViewport && surface.renderInDock !== undefined
366
+ ? surface.renderInDock(width, available) : surface.render(width);
367
+ const body = this.usesDefaultInputSurface() ? this.editor.bodyGeometry?.() : undefined;
364
368
  const rowsWithoutTransient = [...aboveWidgets, ...input, ...belowWidgets, ...footer];
365
369
  const inputOffset = transientRows.length + aboveWidgets.length;
366
370
  return {
@@ -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
- routeHost: routeHost ?? null, dialogs: options.presenters.components.dialogs,
184
+ routeHost: routeHost ?? null, dialogs: options.presenters.components.dialogs, inPlaceSession: this.#customViewport,
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,8 +308,15 @@ 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();
318
+ if (this.#dialogs.handleInputRouteInterrupt(data))
319
+ return { consume: true };
313
320
  if (!this.#terminal.hasOverlay() && !isDefault
314
321
  && (this.root.editor.matchesTerminalKey(data, "pageUp")
315
322
  || this.root.editor.matchesTerminalKey(data, "pageDown"))) {
@@ -881,7 +888,8 @@ export class OwnedUiSessionPresenter {
881
888
  const separator = body.search(/\s/);
882
889
  const name = separator < 0 ? body : body.slice(0, separator);
883
890
  const argument = separator < 0 ? "" : body.slice(separator + 1).trimStart();
884
- if (this.#dialogs.claimsRoute(name))
891
+ const applicationOnly = this.#customViewport && (name === "settings" || name === "changelog" || name === "hotkeys");
892
+ if (!applicationOnly && this.#dialogs.claimsRoute(name))
885
893
  return this.#dialogs.openRoute(name);
886
894
  if (this.#isWorkflowRoute(name))
887
895
  return this.runWorkflow({ command: name, argument });
@@ -915,7 +923,7 @@ export class OwnedUiSessionPresenter {
915
923
  const skills = this.#skillSummaries();
916
924
  const trimmed = argument.trim();
917
925
  if (trimmed.length === 0) {
918
- this.selectors.showSkillsSelector(skills);
926
+ await this.selectors.showSkillsSelector(skills);
919
927
  return { outcome: "completed", diagnostic: null };
920
928
  }
921
929
  const separator = trimmed.search(/\s/u);
@@ -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;