@timurproko/a1 0.1.8-dev.136 → 0.1.8-dev.139

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 (81) hide show
  1. package/README.md +2 -4
  2. package/dist/cli/capabilities.d.ts +2 -11
  3. package/dist/cli/capabilities.js +1 -1
  4. package/dist/cli/dispatch.js +3 -6
  5. package/dist/composition/owned-ui.js +19 -1
  6. package/dist/contracts/owned-ui/model.d.ts +21 -0
  7. package/dist/contracts/owned-ui/validation.js +3 -0
  8. package/dist/features/launch/development-launch.js +11 -3
  9. package/dist/features/launch/index.d.ts +0 -1
  10. package/dist/features/launch/index.js +0 -1
  11. package/dist/features/launch/intent.d.ts +3 -2
  12. package/dist/features/launch/intent.js +1 -2
  13. package/dist/features/launch/prepare-launch.d.ts +0 -2
  14. package/dist/features/launch/prepare-launch.js +2 -9
  15. package/dist/features/launch/profile-paths.d.ts +1 -2
  16. package/dist/features/launch/profile-paths.js +2 -8
  17. package/dist/features/launch/runtime-selection.d.ts +2 -5
  18. package/dist/features/owned-ui/settings-app.js +36 -13
  19. package/dist/foundation/lifecycle/model.d.ts +1 -1
  20. package/dist/foundation/lifecycle/model.js +1 -1
  21. package/dist/foundation/release/bootstrap.d.ts +1 -3
  22. package/dist/foundation/release/bootstrap.js +12 -4
  23. package/dist/foundation/storage/control-store.js +37 -3
  24. package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
  25. package/dist/integrations/pi/components/shell-footer-status.js +14 -5
  26. package/dist/integrations/pi/components/shell-presenters-transcript.d.ts +3 -2
  27. package/dist/integrations/pi/components/shell-presenters-transcript.js +9 -8
  28. package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +13 -0
  29. package/dist/integrations/pi/components/submitted-prompt-adapter.js +27 -0
  30. package/dist/integrations/pi/engine/adapter.js +16 -2
  31. package/dist/integrations/pi/engine/session-integration.js +5 -2
  32. package/dist/integrations/pi/owned-ui/session-shell-root.d.ts +17 -2
  33. package/dist/integrations/pi/owned-ui/session-shell-root.js +451 -42
  34. package/dist/integrations/pi/owned-ui/session-shell.js +116 -13
  35. package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -1
  36. package/dist/integrations/pi/tui-runtime/adapter.js +51 -2
  37. package/dist/integrations/pi/tui-runtime/contracts.d.ts +2 -0
  38. package/dist/native/darwin-arm64/manifest.json +1 -1
  39. package/dist/native/linux-x64/manifest.json +1 -1
  40. package/dist/native/win32-x64/manifest.json +2 -2
  41. package/dist/native/win32-x64/process-guardian.exe +0 -0
  42. package/dist/product-identity.d.ts +2 -2
  43. package/dist/product-identity.js +3 -4
  44. package/dist/product-identity.json +3 -5
  45. package/dist/ui/components/index.d.ts +3 -0
  46. package/dist/ui/components/index.js +3 -0
  47. package/dist/ui/components/mouse.d.ts +10 -0
  48. package/dist/ui/components/mouse.js +23 -0
  49. package/dist/ui/components/prompt-row.d.ts +14 -0
  50. package/dist/ui/components/prompt-row.js +20 -0
  51. package/dist/ui/components/scrollbar.d.ts +26 -0
  52. package/dist/ui/components/scrollbar.js +35 -0
  53. package/dist/ui/components/spans.d.ts +6 -0
  54. package/dist/ui/components/spans.js +36 -0
  55. package/dist/ui/components/submitted-prompt.d.ts +23 -0
  56. package/dist/ui/components/submitted-prompt.js +50 -0
  57. package/dist/ui/components/text-selection.d.ts +53 -0
  58. package/dist/ui/components/text-selection.js +168 -0
  59. package/dist/ui/components/text.d.ts +14 -0
  60. package/dist/ui/components/text.js +40 -0
  61. package/dist/ui/components/transcript-viewport.d.ts +98 -0
  62. package/dist/ui/components/transcript-viewport.js +353 -0
  63. package/dist/ui/settings/declarations.d.ts +8 -1
  64. package/dist/ui/settings/declarations.js +33 -5
  65. package/dist/ui/settings/migrations.d.ts +1 -1
  66. package/dist/ui/settings/migrations.js +12 -2
  67. package/dist/ui/settings/sections.js +15 -8
  68. package/dist/ui/settings/session.d.ts +2 -0
  69. package/dist/ui/settings/session.js +7 -5
  70. package/docs/architecture/boundaries.md +2 -2
  71. package/docs/architecture/project-structure.md +1 -1
  72. package/docs/architecture/resource-and-data-policy.md +1 -1
  73. package/docs/architecture/toolchain.md +5 -5
  74. package/docs/architecture/ui-reference-provenance.md +11 -2
  75. package/docs/features/launch-profiles.md +7 -14
  76. package/docs/manual-launch-instance-acceptance.md +12 -35
  77. package/docs/manual-owned-ui-checkpoint.md +2 -3
  78. package/docs/manual-pi-boundary-candidate.md +4 -5
  79. package/package.json +1 -2
  80. package/dist/features/launch/profiles.d.ts +0 -12
  81. package/dist/features/launch/profiles.js +0 -26
package/README.md CHANGED
@@ -31,17 +31,16 @@ a1 update:107 # install numbered preview 107
31
31
  a1 update --models # refresh A1's model catalogs
32
32
  ```
33
33
 
34
- Development previews add two extra profiles; release builds do not carry them.
34
+ Development previews add the Pi comparison profile; release builds do not carry it.
35
35
 
36
36
  ```sh
37
37
  a1 pi # vanilla Pi oracle: ~/.pi/agent
38
- a1 sandbox # isolated vanilla Pi profile: ~/.a1/sandbox
39
38
  ```
40
39
 
41
40
  ## Extensions
42
41
 
43
42
  Pi extension packages install into A1's own profile (`~/.a1/agent`), so bare `a1`
44
- loads them and `a1 pi` / `a1 sandbox` do not. Sources are Pi's: `npm:`, git, or a
43
+ loads them and `a1 pi` does not. Sources are Pi's: `npm:`, git, or a
45
44
  local path.
46
45
 
47
46
  ```sh
@@ -63,7 +62,6 @@ npm ci # install exact locked dependencies
63
62
  npm run build # compile TypeScript and the process guardian into dist
64
63
  npm start # build and launch a development `a1`
65
64
  npm run start:pi # build and launch a development `a1 pi`
66
- npm run start:sandbox # build and launch a development `a1 sandbox`
67
65
  npm run test:fast # typecheck + fast suite (alias: npm test)
68
66
  npm run test:full # complete non-physical suite
69
67
  ```
@@ -1,15 +1,6 @@
1
- /**
2
- * `pi` and `sandbox` exist to compare A1 against pinned Pi and to try resources
3
- * against an isolated profile. Both are development instruments rather than
4
- * product, so a stable release does not carry them: what a released `a1` exposes
5
- * is the product plus its maintenance and package commands.
6
- *
7
- * Which build this is comes from its own version. A prerelease version is a
8
- * `next`-channel build, which is where that work happens; a release version is
9
- * not. Nothing to configure, and no way for a released build to be talked into it.
10
- */
1
+ /** The Pi comparison launch is a prerelease development instrument, not product. */
11
2
  export interface CliCapabilities {
12
- readonly developmentProfiles: boolean;
3
+ readonly developmentComparison: boolean;
13
4
  }
14
5
  export declare function cliCapabilities(version: string): CliCapabilities;
15
6
  export declare function isPrereleaseVersion(version: string): boolean;
@@ -1,6 +1,6 @@
1
1
  import { prerelease } from "semver";
2
2
  export function cliCapabilities(version) {
3
- return Object.freeze({ developmentProfiles: isPrereleaseVersion(version) });
3
+ return Object.freeze({ developmentComparison: isPrereleaseVersion(version) });
4
4
  }
5
5
  export function isPrereleaseVersion(version) {
6
6
  return (prerelease(version)?.length ?? 0) > 0;
@@ -3,7 +3,7 @@ import { PRODUCT_TEXT } from "../product-identity.js";
3
3
  export function cliUsage(capabilities) {
4
4
  return PRODUCT_TEXT.usage([
5
5
  "",
6
- ...(capabilities.developmentProfiles ? ["pi", "sandbox"] : []),
6
+ ...(capabilities.developmentComparison ? ["pi"] : []),
7
7
  "version",
8
8
  "update [self|--models]",
9
9
  "update:develop",
@@ -14,7 +14,7 @@ export function cliUsage(capabilities) {
14
14
  "pi update [--extensions|<source>]",
15
15
  ]);
16
16
  }
17
- const PROFILE_WORDS = new Set(["pi", "sandbox"]);
17
+ const PROFILE_WORDS = new Set(["pi"]);
18
18
  export async function dispatchCli(arguments_, handlers, output, capabilities) {
19
19
  const command = parseCliCommand(arguments_, capabilities);
20
20
  if (command.kind === "error") {
@@ -36,12 +36,9 @@ export function parseCliCommand(arguments_, capabilities) {
36
36
  if (command === "pi") {
37
37
  if (rest.length > 0)
38
38
  return parsePiPackageCommand(rest);
39
- if (capabilities.developmentProfiles)
39
+ if (capabilities.developmentComparison)
40
40
  return { kind: "launch", profileId: "pi" };
41
41
  }
42
- if (capabilities.developmentProfiles && command === "sandbox") {
43
- return withoutArguments(rest, { kind: "launch", profileId: command });
44
- }
45
42
  if (command === "version")
46
43
  return withoutArguments(rest, { kind: "version" });
47
44
  if (command !== undefined && command.startsWith("update:"))
@@ -18,16 +18,24 @@ export async function composeOwnedUi(options = {}) {
18
18
  : new OwnedUiSettingsSession({
19
19
  store: new OwnedUiSettingsStore({ configDir: resolveProductPaths().configDir, profileId: options.profileId }),
20
20
  agentProvider: () => adapter.settingsPort(),
21
+ ...(options.ownedSurfaces === "off" ? {} : { hiddenAgentSettingIds: ["tuiMode"] }),
21
22
  });
22
23
  // Before anything is drawn, so every surface uses the configured theme rather
23
24
  // than the one guessed from the terminal's background.
24
25
  applyConfiguredPiTheme(adapter.configuredTheme());
25
- const routeHost = settings === null || options.ownedSurfaces === "off" ? null : createOwnedRouteHost(settings);
26
+ const ownedSurfaces = options.ownedSurfaces !== "off";
27
+ const routeHost = settings === null || !ownedSurfaces ? null : createOwnedRouteHost(settings);
28
+ const viewportSettings = settings === null || !ownedSurfaces ? null : {
29
+ snapshot: () => viewportSettingsSnapshot(settings),
30
+ onChange: listener => settings.onChange(() => listener(viewportSettingsSnapshot(settings))),
31
+ };
26
32
  const shell = new OwnedUiSessionShell({
27
33
  backend: adapter,
28
34
  cwd,
29
35
  ...(options.terminal === undefined ? {} : { terminal: createPiTerminalBridge(options.terminal) }),
30
36
  ...(routeHost === null ? {} : { routeHost }),
37
+ ...(ownedSurfaces ? { sessionLayout: "custom-viewport" } : {}),
38
+ ...(viewportSettings === null ? {} : { viewportSettings }),
31
39
  });
32
40
  const application = {
33
41
  get disposed() { return adapter.disposed; },
@@ -38,3 +46,13 @@ export async function composeOwnedUi(options = {}) {
38
46
  };
39
47
  return { application, settings };
40
48
  }
49
+ function viewportSettingsSnapshot(settings) {
50
+ const appearance = settings.value("scrollbarAppearance");
51
+ const style = settings.value("scrollbarStyle");
52
+ const speed = settings.value("scrollbarSpeed");
53
+ return {
54
+ scrollbarAppearance: appearance === "always" || appearance === "hidden" ? appearance : "hover",
55
+ scrollbarStyle: style === "thick" ? "thick" : "thin",
56
+ scrollbarSpeed: speed === "high" ? "high" : speed === "fast" ? "fast" : "normal",
57
+ };
58
+ }
@@ -12,6 +12,17 @@ export interface OwnedUiModelInfo {
12
12
  readonly displayName: string;
13
13
  }
14
14
  export type OwnedUiThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
15
+ export interface OwnedUiViewportSettings {
16
+ readonly scrollbarAppearance: "always" | "hover" | "hidden";
17
+ readonly scrollbarStyle: "thin" | "thick";
18
+ /** normal is baseline; fast is 2x normal; high is normal plus fast. */
19
+ readonly scrollbarSpeed: "normal" | "fast" | "high";
20
+ }
21
+ /** Narrow live settings boundary consumed by the bare-A1 shell composition. */
22
+ export interface OwnedUiViewportSettingsPort {
23
+ snapshot(): OwnedUiViewportSettings;
24
+ onChange(listener: (settings: OwnedUiViewportSettings) => void): () => void;
25
+ }
15
26
  export interface OwnedUiTerminalSurface {
16
27
  readonly columns: number;
17
28
  readonly rows: number;
@@ -189,6 +200,16 @@ export type OwnedUiEvent = {
189
200
  readonly sessionId: OwnedUiSessionId;
190
201
  readonly sequence: number;
191
202
  readonly block: OwnedUiTranscriptBlock;
203
+ } | {
204
+ /** One completed assistant reply; never a tool row or transcript rebuild. */
205
+ readonly type: "assistant-message-completed";
206
+ readonly sessionId: OwnedUiSessionId;
207
+ readonly sequence: number;
208
+ } | {
209
+ /** A fresh agent run has started, used by follow-mode surfaces. */
210
+ readonly type: "agent-run-started";
211
+ readonly sessionId: OwnedUiSessionId;
212
+ readonly sequence: number;
192
213
  } | {
193
214
  readonly type: "editor-state";
194
215
  readonly sessionId: OwnedUiSessionId;
@@ -103,6 +103,9 @@ export function assertOwnedUiEvent(event) {
103
103
  case "transcript-block":
104
104
  assertOwnedUiTranscriptBlock(event.block);
105
105
  return;
106
+ case "assistant-message-completed":
107
+ case "agent-run-started":
108
+ return;
106
109
  case "editor-state":
107
110
  assertOwnedUiEditorState(event.editor);
108
111
  return;
@@ -8,8 +8,16 @@ export function resolveDevelopmentLaunchEnvironment(canonicalPackageRoot, releas
8
8
  const instanceId = selectedInstanceId
9
9
  ? createHash("sha256").update(selectedInstanceId).digest("hex").slice(0, 16)
10
10
  : randomUUID().replaceAll("-", "").slice(0, 16);
11
- const developmentRoot = resolve(source[PRODUCT_IDENTITY.environment.developmentRoot]
12
- ?? join(temporaryDirectory, PRODUCT_IDENTITY.state.developmentDirectory, checkoutId, releaseId, "instances", instanceId));
11
+ const checkoutRoot = resolve(temporaryDirectory, PRODUCT_IDENTITY.state.developmentDirectory, checkoutId);
12
+ const selectedDevelopmentRoot = source[PRODUCT_IDENTITY.environment.developmentRoot];
13
+ const developmentRoot = resolve(selectedDevelopmentRoot
14
+ ?? join(checkoutRoot, releaseId, "instances", instanceId));
15
+ // Runtime/data remain instance-isolated, but product settings are user
16
+ // preferences and must survive a fresh local launch (and a rebuilt candidate).
17
+ // An explicit development root retains its historical self-contained policy.
18
+ const persistentConfigDir = selectedDevelopmentRoot
19
+ ? join(developmentRoot, "config")
20
+ : join(checkoutRoot, "config");
13
21
  const dataDir = resolve(source[PRODUCT_IDENTITY.environment.dataDir] ?? join(developmentRoot, "data"));
14
22
  return {
15
23
  checkoutId,
@@ -19,7 +27,7 @@ export function resolveDevelopmentLaunchEnvironment(canonicalPackageRoot, releas
19
27
  environment: {
20
28
  ...source,
21
29
  [PRODUCT_IDENTITY.environment.developmentInstanceId]: instanceId,
22
- [PRODUCT_IDENTITY.environment.configDir]: resolve(source[PRODUCT_IDENTITY.environment.configDir] ?? join(developmentRoot, "config")),
30
+ [PRODUCT_IDENTITY.environment.configDir]: resolve(source[PRODUCT_IDENTITY.environment.configDir] ?? persistentConfigDir),
23
31
  [PRODUCT_IDENTITY.environment.dataDir]: dataDir,
24
32
  [PRODUCT_IDENTITY.environment.runtimeDir]: resolve(source[PRODUCT_IDENTITY.environment.runtimeDir] ?? join(developmentRoot, "runtime")),
25
33
  [PRODUCT_IDENTITY.environment.databasePath]: resolve(source[PRODUCT_IDENTITY.environment.databasePath] ?? join(dataDir, PRODUCT_IDENTITY.endpoint.databaseFilename)),
@@ -3,5 +3,4 @@ export * from "./initialize-profile.js";
3
3
  export * from "./intent.js";
4
4
  export * from "./profile-paths.js";
5
5
  export * from "./prepare-launch.js";
6
- export * from "./profiles.js";
7
6
  export * from "./runtime-selection.js";
@@ -3,5 +3,4 @@ export * from "./initialize-profile.js";
3
3
  export * from "./intent.js";
4
4
  export * from "./profile-paths.js";
5
5
  export * from "./prepare-launch.js";
6
- export * from "./profiles.js";
7
6
  export * from "./runtime-selection.js";
@@ -1,7 +1,8 @@
1
- import { type LaunchProfileContract, type LaunchProfileId } from "./profiles.js";
1
+ import type { LaunchProfileId } from "../../foundation/lifecycle/index.js";
2
+ export type { LaunchProfileId } from "../../foundation/lifecycle/index.js";
2
3
  export interface InteractiveLaunchIntent {
3
4
  readonly kind: "interactive";
4
- readonly profile: LaunchProfileContract;
5
+ readonly profileId: LaunchProfileId;
5
6
  }
6
7
  export type LaunchIntent = InteractiveLaunchIntent;
7
8
  export declare function interactiveLaunchIntent(profileId: LaunchProfileId): InteractiveLaunchIntent;
@@ -1,4 +1,3 @@
1
- import { launchProfileContract } from "./profiles.js";
2
1
  export function interactiveLaunchIntent(profileId) {
3
- return Object.freeze({ kind: "interactive", profile: launchProfileContract(profileId) });
2
+ return Object.freeze({ kind: "interactive", profileId });
4
3
  }
@@ -2,9 +2,7 @@ import { type LaunchProfilePathOptions } from "./profile-paths.js";
2
2
  import { initializeProductProfile } from "./initialize-profile.js";
3
3
  import type { InteractiveLaunchIntent } from "./intent.js";
4
4
  export interface PreparedInteractiveLaunch {
5
- readonly intent: InteractiveLaunchIntent;
6
5
  readonly environment: NodeJS.ProcessEnv;
7
- readonly piArguments: readonly string[];
8
6
  readonly configurationRoot: string | null;
9
7
  }
10
8
  export interface PrepareInteractiveLaunchOptions extends LaunchProfilePathOptions {
@@ -2,7 +2,7 @@ import { configurationRootForProfile, resolveLaunchProfilePaths } from "./profil
2
2
  import { initializeProductProfile } from "./initialize-profile.js";
3
3
  export async function prepareInteractiveLaunch(intent, environment = process.env, options = {}) {
4
4
  const paths = resolveLaunchProfilePaths({ ...options, environment });
5
- const configurationRoot = configurationRootForProfile(intent.profile.id, paths);
5
+ const configurationRoot = configurationRootForProfile(intent.profileId, paths);
6
6
  if (configurationRoot !== null)
7
7
  await (options.initializeProfile ?? initializeProductProfile)(configurationRoot);
8
8
  const childEnvironment = { ...environment };
@@ -10,12 +10,5 @@ export async function prepareInteractiveLaunch(intent, environment = process.env
10
10
  delete childEnvironment.PI_CODING_AGENT_DIR;
11
11
  else
12
12
  childEnvironment.PI_CODING_AGENT_DIR = configurationRoot;
13
- const piArguments = intent.profile.projectTrust === "ignore" ? ["--no-approve"] : [];
14
- childEnvironment.A1_LAUNCH_ARGUMENTS_JSON = JSON.stringify(piArguments);
15
- return Object.freeze({
16
- intent,
17
- environment: childEnvironment,
18
- piArguments: Object.freeze(piArguments),
19
- configurationRoot,
20
- });
13
+ return Object.freeze({ environment: childEnvironment, configurationRoot });
21
14
  }
@@ -1,9 +1,8 @@
1
- import type { LaunchProfileId } from "./profiles.js";
1
+ import type { LaunchProfileId } from "./intent.js";
2
2
  export interface LaunchProfilePaths {
3
3
  readonly home: string;
4
4
  readonly managedStateRoot: string;
5
5
  readonly agentProfile: string;
6
- readonly sandbox: string;
7
6
  }
8
7
  export interface LaunchProfilePathOptions {
9
8
  readonly environment?: NodeJS.ProcessEnv;
@@ -7,20 +7,14 @@ export function resolveLaunchProfilePaths(options = {}) {
7
7
  const selectedHome = options.home ?? environment[PRODUCT_IDENTITY.environment.profileHome] ?? options.readHome?.() ?? homedir();
8
8
  const home = validateAbsolutePath(selectedHome, "effective user home", path);
9
9
  const agentProfile = path.resolve(home, PRODUCT_IDENTITY.state.piAgentProfile);
10
- const managedStateRoot = path.dirname(agentProfile);
11
10
  return Object.freeze({
12
11
  home,
13
- managedStateRoot,
12
+ managedStateRoot: path.dirname(agentProfile),
14
13
  agentProfile,
15
- sandbox: path.resolve(home, PRODUCT_IDENTITY.state.piSandboxProfile),
16
14
  });
17
15
  }
18
16
  export function configurationRootForProfile(profileId, paths) {
19
- if (profileId === "a1")
20
- return paths.agentProfile;
21
- if (profileId === "sandbox")
22
- return paths.sandbox;
23
- return null;
17
+ return profileId === "a1" ? paths.agentProfile : null;
24
18
  }
25
19
  function validateAbsolutePath(value, name, path) {
26
20
  if (value.length === 0 || value.includes("\0"))
@@ -1,10 +1,7 @@
1
1
  import { type LaunchProfileId } from "../../foundation/lifecycle/index.js";
2
2
  /**
3
- * Every interactive command is the same composition. What differs is the
4
- * configuration root each one reads and whether A1's own screens are reachable:
5
- * bare `a1` is the product, while `a1 pi` and `a1 sandbox` present pinned Pi's
6
- * interface and nothing of A1's own, against Pi's own profile and against an
7
- * isolated one.
3
+ * Both interactive commands use the owned composition. Bare A1 enables product
4
+ * surfaces; the Pi comparison command withholds them and reads Pi's own profile.
8
5
  */
9
6
  export type OwnedUiProfileId = LaunchProfileId;
10
7
  export interface InteractiveRuntimeSelection {
@@ -56,6 +56,8 @@ export class SettingsApp {
56
56
  #session;
57
57
  #selectedKey;
58
58
  #scroll = 0;
59
+ /** Keyboard navigation requests visibility once; pointer scrolling then stays free. */
60
+ #selectionNeedsReveal = true;
59
61
  #reveal;
60
62
  #notice = null;
61
63
  #filter = null;
@@ -96,9 +98,13 @@ export class SettingsApp {
96
98
  this.#panelTop = this.#panelTopForFrame;
97
99
  const bodyHeight = Math.max(0, rect.height - footer.length);
98
100
  const selected = indexOfKey(rows, this.#selectedKey);
99
- this.#scroll = scrollForSelection(rows, bodyHeight, this.#scroll, selected, this.#reveal);
101
+ if (this.#selectionNeedsReveal) {
102
+ this.#scroll = scrollForSelection(rows, bodyHeight, this.#scroll, selected, this.#reveal);
103
+ this.#selectionNeedsReveal = false;
104
+ }
100
105
  this.#reveal = undefined;
101
106
  const layout = layoutList(rows, bodyHeight, this.#scroll);
107
+ this.#scroll = layout.scroll;
102
108
  const contentWidth = Math.max(0, rect.width - RAIL_COLUMNS);
103
109
  const valueColumn = this.#valueColumn(rows);
104
110
  const geometry = scrollbarGeometry({
@@ -121,14 +127,13 @@ export class SettingsApp {
121
127
  for (const index of layout.rowIndexes) {
122
128
  const row = rows[index];
123
129
  if (row !== undefined && row.kind === "element") {
130
+ const view = this.#viewRow(row.value);
124
131
  this.#frameRows.push({
125
- key: `${row.value.backend}:${row.value.id}`,
132
+ key: view.key,
126
133
  screenRow: body.length,
127
134
  valueColumn,
128
- valueWidth: row.value.structured
129
- ? CONFIGURE.length
130
- : row.value.value === null ? 0 : displayWidth(displayValue(row.value.value)),
131
- stepper: isStepper(row.value),
135
+ valueWidth: displayWidth(view.value),
136
+ stepper: view.stepper !== undefined,
132
137
  });
133
138
  }
134
139
  body.push(this.#renderRow(row, index === selected, contentWidth, valueColumn, theme));
@@ -189,8 +194,11 @@ export class SettingsApp {
189
194
  // End lands on the very last setting, not on the head of its section.
190
195
  const selectable = selectableIndexes(rows);
191
196
  const target = action === "last" ? selectable.at(-1) : selectable[0];
192
- if (target !== undefined)
193
- this.#jump(rows, target);
197
+ if (target !== undefined) {
198
+ this.#select(rows, target);
199
+ if (action === "first")
200
+ this.#scroll = 0;
201
+ }
194
202
  return { consumed: true };
195
203
  }
196
204
  case "previous-value":
@@ -263,6 +271,14 @@ export class SettingsApp {
263
271
  }
264
272
  return { consumed: true, render: false };
265
273
  }
274
+ if (event.kind === "wheel-up" || event.kind === "wheel-down") {
275
+ // The whole list pane owns wheel scrolling, including blank space beside
276
+ // short labels. It must not depend on finding an item under the pointer.
277
+ if (event.row < 1 || event.row > this.#panelTopForFrame)
278
+ return { consumed: false };
279
+ this.#scroll = Math.max(0, this.#scroll + (event.kind === "wheel-down" ? 3 : -3));
280
+ return { consumed: true };
281
+ }
266
282
  const row = this.#frameRows.find(candidate => candidate.screenRow === event.row - 1);
267
283
  const previousKey = this.#hoverKey;
268
284
  const previousRegion = this.#hoverRegion;
@@ -288,10 +304,6 @@ export class SettingsApp {
288
304
  }
289
305
  return { consumed: true };
290
306
  }
291
- if (event.kind === "wheel-up" || event.kind === "wheel-down") {
292
- this.#scroll = Math.max(0, this.#scroll + (event.kind === "wheel-down" ? 3 : -3));
293
- return { consumed: true };
294
- }
295
307
  const changed = previousKey !== this.#hoverKey || previousRegion !== this.#hoverRegion;
296
308
  return { consumed: event.kind !== "motion", render: changed };
297
309
  }
@@ -403,6 +415,17 @@ export class SettingsApp {
403
415
  if (input === null)
404
416
  return { consumed: false };
405
417
  const key = KEYS[data];
418
+ if (key === "home" || key === "end") {
419
+ const rows = this.#rows();
420
+ const selectable = selectableIndexes(rows);
421
+ const target = key === "end" ? selectable.at(-1) : selectable[0];
422
+ if (target !== undefined) {
423
+ this.#select(rows, target);
424
+ if (key === "home")
425
+ this.#scroll = 0;
426
+ }
427
+ return { consumed: true };
428
+ }
406
429
  if (key === "up" || key === "down" || key === "shift+up" || key === "shift+down") {
407
430
  const rows = this.#rows();
408
431
  // Nothing found means nothing to move through; the key is still swallowed
@@ -485,6 +508,7 @@ export class SettingsApp {
485
508
  if (index < 0)
486
509
  return;
487
510
  this.#selectedKey = rowKey(rows[index]);
511
+ this.#selectionNeedsReveal = true;
488
512
  this.#notice = null;
489
513
  }
490
514
  #rows() {
@@ -554,7 +578,6 @@ export class SettingsApp {
554
578
  key: `${entry.backend}:${entry.id}`,
555
579
  label: labelOf(entry),
556
580
  value,
557
- ...(entry.origin === "default" ? { suffix: "(default)" } : {}),
558
581
  ...(typeof shown === "number" && entry.editable ? { stepper: stepperEnds(range, shown) } : {}),
559
582
  };
560
583
  }
@@ -1,4 +1,4 @@
1
- export type LaunchProfileId = "a1" | "pi" | "sandbox";
1
+ export type LaunchProfileId = "a1" | "pi";
2
2
  export type GenerationId = string;
3
3
  export type DriverProfileId = string;
4
4
  export type RequestId = string;
@@ -1,6 +1,6 @@
1
1
  import { PRODUCT_TEXT } from "../../product-identity.js";
2
2
  export function assertLaunchProfileId(value) {
3
- if (value !== "a1" && value !== "pi" && value !== "sandbox")
3
+ if (value !== "a1" && value !== "pi")
4
4
  throw new TypeError(PRODUCT_TEXT.diagnostic(`launch profile is invalid: ${String(value)}`));
5
5
  }
6
6
  export function assertDimensions(dimensions) {
@@ -7,9 +7,7 @@ export interface BootstrapOptions {
7
7
  readonly packageRoot: string;
8
8
  readonly launchIntent?: {
9
9
  readonly kind: "interactive";
10
- readonly profile: {
11
- readonly id: LaunchProfileId;
12
- };
10
+ readonly profileId: LaunchProfileId;
13
11
  };
14
12
  readonly environment?: NodeJS.ProcessEnv;
15
13
  readonly output?: Pick<NodeJS.WriteStream, "write">;
@@ -13,7 +13,7 @@ import { consumeMaterializationProof, materializeRelease, readCertifiedReleaseMa
13
13
  import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
14
14
  export async function runBootstrap(options) {
15
15
  const environment = { ...(options.environment ?? process.env) };
16
- const launchProfileId = options.launchIntent?.profile.id ?? "a1";
16
+ const launchProfileId = options.launchIntent?.profileId ?? "a1";
17
17
  assertLaunchProfileId(launchProfileId);
18
18
  environment[PRODUCT_IDENTITY.environment.launchProfile] = launchProfileId;
19
19
  const output = options.output ?? process.stderr;
@@ -47,9 +47,6 @@ export async function runBootstrap(options) {
47
47
  const activeId = state.references.active;
48
48
  const active = activeId === null ? undefined : state.releases[activeId];
49
49
  const activeIsLaunchable = active === undefined || (options.releaseIsLaunchable?.(active.releaseRoot) ?? true);
50
- if (active !== undefined && !activeIsLaunchable) {
51
- output.write(`${PRODUCT_TEXT.diagnostic(`retired release ${active.releaseId} because its copy is not launchable; rebuilding from the installed package.`)}\n`);
52
- }
53
50
  if (activeIsLaunchable && active?.approval === "approved" && active.packageVersion === installedVersion) {
54
51
  const endpointMatches = endpoint?.releaseId === active.releaseId
55
52
  && endpoint.releaseRoot === active.releaseRoot
@@ -77,6 +74,17 @@ export async function runBootstrap(options) {
77
74
  await stateStore.activate(candidate.releaseId);
78
75
  state = await stateStore.read();
79
76
  }
77
+ else if (!activeIsLaunchable && candidate.releaseId !== activeId
78
+ && state.releases[candidate.releaseId]?.approval !== "approved") {
79
+ // The active reference points at a copy that cannot launch, so reusing it
80
+ // is off the table — but an unapproved candidate would lose the selection
81
+ // below to that same broken active (`start-active`). Approving the healed
82
+ // candidate here lets ordinary cohort selection activate it, while a live
83
+ // busy cohort still wins the endpoint checks and keeps its sessions.
84
+ const diagnosticsPath = await certifyMaterializedRelease(candidate, paths.dataDir);
85
+ await stateStore.approve(candidate.releaseId, diagnosticsPath);
86
+ state = await stateStore.read();
87
+ }
80
88
  // The candidate's own endpoint decides whether this launch attaches or starts a supervisor.
81
89
  // A cohort other than this one is not in the way: it listens somewhere else.
82
90
  endpointPaths = resolveCohortEndpoint(paths, candidate.releaseId, environment);
@@ -30,8 +30,8 @@ export class ControlStore {
30
30
  migrate() {
31
31
  const versionRow = this.database.prepare("PRAGMA user_version").get();
32
32
  const version = versionRow.user_version;
33
- if (version > 5)
34
- throw new Error(`control database version ${version} is newer than supported version 5`);
33
+ if (version > 6)
34
+ throw new Error(`control database version ${version} is newer than supported version 6`);
35
35
  if (version !== 0)
36
36
  this.#assertCurrentControlSchema();
37
37
  if (version === 0) {
@@ -187,7 +187,7 @@ export class ControlStore {
187
187
  CREATE TABLE launch_instances (
188
188
  id TEXT PRIMARY KEY,
189
189
  owner_client_id TEXT NOT NULL,
190
- profile_id TEXT NOT NULL CHECK (profile_id IN ('a1', 'pi', 'sandbox')),
190
+ profile_id TEXT NOT NULL CHECK (profile_id IN ('a1', 'pi')),
191
191
  state TEXT NOT NULL CHECK (state IN ('requested', 'active', 'stopping', 'completed', 'interrupted')),
192
192
  shutdown_policy TEXT NOT NULL CHECK (shutdown_policy = 'terminate-tree-on-close'),
193
193
  guardian_identity_json TEXT NOT NULL CHECK (json_valid(guardian_identity_json)),
@@ -208,6 +208,40 @@ export class ControlStore {
208
208
  CREATE INDEX idx_launch_instances_boot ON launch_instances(owner_boot_nonce, state);
209
209
  PRAGMA user_version = 5;
210
210
  COMMIT;
211
+ `);
212
+ }
213
+ if (version <= 5) {
214
+ this.database.exec(`
215
+ BEGIN IMMEDIATE;
216
+ ALTER TABLE launch_instances RENAME TO launch_instances_v5;
217
+ DROP INDEX idx_launch_instances_owner;
218
+ DROP INDEX idx_launch_instances_boot;
219
+ CREATE TABLE launch_instances (
220
+ id TEXT PRIMARY KEY,
221
+ owner_client_id TEXT NOT NULL,
222
+ profile_id TEXT NOT NULL CHECK (profile_id IN ('a1', 'pi')),
223
+ state TEXT NOT NULL CHECK (state IN ('requested', 'active', 'stopping', 'completed', 'interrupted')),
224
+ shutdown_policy TEXT NOT NULL CHECK (shutdown_policy = 'terminate-tree-on-close'),
225
+ guardian_identity_json TEXT NOT NULL CHECK (json_valid(guardian_identity_json)),
226
+ root_identity_json TEXT CHECK (root_identity_json IS NULL OR json_valid(root_identity_json)),
227
+ containment_identity_json TEXT CHECK (containment_identity_json IS NULL OR json_valid(containment_identity_json)),
228
+ created_at TEXT NOT NULL,
229
+ activated_at TEXT,
230
+ stopping_at TEXT,
231
+ completed_at TEXT,
232
+ outcome_json TEXT CHECK (outcome_json IS NULL OR json_valid(outcome_json)),
233
+ owner_boot_nonce TEXT NOT NULL,
234
+ CHECK ((state = 'requested' AND root_identity_json IS NULL AND containment_identity_json IS NULL AND activated_at IS NULL AND stopping_at IS NULL AND completed_at IS NULL AND outcome_json IS NULL)
235
+ OR (state = 'active' AND root_identity_json IS NOT NULL AND containment_identity_json IS NOT NULL AND activated_at IS NOT NULL AND stopping_at IS NULL AND completed_at IS NULL AND outcome_json IS NULL)
236
+ OR (state = 'stopping' AND root_identity_json IS NOT NULL AND containment_identity_json IS NOT NULL AND activated_at IS NOT NULL AND stopping_at IS NOT NULL AND completed_at IS NULL AND outcome_json IS NULL)
237
+ OR (state IN ('completed', 'interrupted') AND completed_at IS NOT NULL AND outcome_json IS NOT NULL))
238
+ );
239
+ INSERT INTO launch_instances SELECT * FROM launch_instances_v5 WHERE profile_id IN ('a1', 'pi');
240
+ DROP TABLE launch_instances_v5;
241
+ CREATE INDEX idx_launch_instances_owner ON launch_instances(owner_client_id, state);
242
+ CREATE INDEX idx_launch_instances_boot ON launch_instances(owner_boot_nonce, state);
243
+ PRAGMA user_version = 6;
244
+ COMMIT;
211
245
  `);
212
246
  }
213
247
  if (version === 0) {
@@ -4,4 +4,4 @@ export declare function createPiShellHeader(options?: PiShellHeaderOptions): PiS
4
4
  export declare function createPiShellLoadedResources(resources: readonly PiShellResourceEntry[], initialExpanded?: boolean): PiShellLoadedResourcesPort;
5
5
  export declare function createPiShellStatus(view: OwnedUiSessionViewModel, runtime?: Pick<PiShellEditorOptions, "getColumns" | "getRows" | "requestRender">): PiShellStatusPort;
6
6
  export declare function createPiShellFooter(view: OwnedUiSessionViewModel, cwd: string): PiShellViewComponentPort;
7
- export declare function createPiQueuedInputStatus(submissions: readonly string[]): PiShellQueuedInputPort;
7
+ export declare function createPiQueuedInputStatus(submissions: readonly string[], presentation?: "pinned" | "custom-viewport"): PiShellQueuedInputPort;
@@ -110,14 +110,14 @@ export function createPiShellFooter(view, cwd) {
110
110
  dispose: () => footer.dispose(),
111
111
  };
112
112
  }
113
- export function createPiQueuedInputStatus(submissions) {
114
- const text = new Text(queuedInputText(submissions), 1, 0);
113
+ export function createPiQueuedInputStatus(submissions, presentation = "pinned") {
114
+ const text = new Text(queuedInputText(submissions, presentation), 1, 0);
115
115
  return {
116
116
  render: width => submissions.length === 0 ? [] : text.render(width),
117
117
  invalidate: () => text.invalidate(),
118
118
  update(next) {
119
119
  submissions = next;
120
- text.setText(queuedInputText(next));
120
+ text.setText(queuedInputText(next, presentation));
121
121
  },
122
122
  };
123
123
  }
@@ -135,9 +135,18 @@ function statusComponent(view, ui, workingOverride) {
135
135
  function statusSignature(view, workingOverride) {
136
136
  return `${view.lifecycle}\u0000${workingOverride ?? ""}\u0000${view.status.workingMessage ?? ""}\u0000${view.status.diagnostics.at(-1) ?? ""}`;
137
137
  }
138
- function queuedInputText(submissions) {
138
+ function queuedInputText(submissions, presentation) {
139
+ if (submissions.length === 0)
140
+ return "";
139
141
  const theme = piTheme();
140
- return submissions.map(submission => theme.fg("muted", `Steering: ${submission.replaceAll("\n", " ⏎ ")}`)).join("\n");
142
+ if (presentation === "pinned") {
143
+ return submissions.map(submission => theme.fg("muted", `Steering: ${submission.replaceAll("\n", " ⏎ ")}`)).join("\n");
144
+ }
145
+ const messages = submissions.map(submission => theme.fg("dim", `Steering: ${submission.replaceAll("\n", " ⏎ ")}`));
146
+ const dequeueHint = theme.fg("dim", "↳ Alt+Up to edit all queued messages");
147
+ // The custom viewport matches Pi's interactive queue presentation while the
148
+ // comparison shell remains byte-for-byte compatible with its pinned fixture.
149
+ return ["", ...messages, dequeueHint].join("\n");
141
150
  }
142
151
  function compactHeaderText() {
143
152
  const theme = piTheme();
@@ -1,5 +1,7 @@
1
1
  import { AssistantMessageComponent } from "@earendil-works/pi-coding-agent";
2
2
  import type { OwnedUiTranscriptBlock } from "../../../contracts/owned-ui/index.js";
3
+ import { type PiShellSubmittedPromptComposer } from "./submitted-prompt-adapter.js";
4
+ export type { PiShellSubmittedPromptComposer } from "./submitted-prompt-adapter.js";
3
5
  import { type PiShellComponentPort, type PiShellExtensionRendererResolver, type PiShellTranscriptComponentPort } from "./shell-shared-facade.js";
4
6
  export interface PiShellSessionInfoPresentation {
5
7
  readonly sessionName?: string;
@@ -35,7 +37,7 @@ export declare function renderPiShellStatusText(message: string, width: number):
35
37
  export declare function createPiShellSessionInfo(presentation: PiShellSessionInfoPresentation): PiShellComponentPort;
36
38
  export declare function createPiShellChangelog(markdown: string): PiShellComponentPort;
37
39
  export declare function createPiShellHotkeys(): PiShellComponentPort;
38
- export declare function createPiShellTranscriptComponent(initial: OwnedUiTranscriptBlock, cwd: string, extensions?: PiShellExtensionRendererResolver): PiShellTranscriptComponentPort;
40
+ export declare function createPiShellTranscriptComponent(initial: OwnedUiTranscriptBlock, cwd: string, extensions?: PiShellExtensionRendererResolver, submittedPrompt?: PiShellSubmittedPromptComposer): PiShellTranscriptComponentPort;
39
41
  export declare function renderPiShellTranscriptBlock(block: OwnedUiTranscriptBlock, width: number, cwd: string): readonly string[];
40
42
  /**
41
43
  * Pinned Pi's CLI prints startup diagnostics with `reportDiagnostics` before
@@ -54,4 +56,3 @@ export declare function renderPiShellStartupDiagnostic(diagnostic: {
54
56
  export declare function renderPiShellPackageUpdateNotice(packages: readonly string[], width: number): readonly string[];
55
57
  type PiAssistantMessage = NonNullable<ConstructorParameters<typeof AssistantMessageComponent>[0]>;
56
58
  export declare function validatedAssistantMessage(block: OwnedUiTranscriptBlock): PiAssistantMessage;
57
- export {};