@timurproko/a1 0.1.1-dev.7 → 0.1.1-dev.9

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 (49) hide show
  1. package/README.md +11 -74
  2. package/bin/{a1-guardian.js → guardian.js} +1 -1
  3. package/bin/{a1-ui.js → ui.js} +1 -8
  4. package/dist/native/darwin-arm64/manifest.json +3 -3
  5. package/dist/native/darwin-arm64/{a1-process-guardian → process-guardian} +0 -0
  6. package/dist/native/linux-x64/manifest.json +3 -3
  7. package/dist/native/linux-x64/{a1-process-guardian → process-guardian} +0 -0
  8. package/dist/native/win32-x64/manifest.json +3 -3
  9. package/dist/native/win32-x64/process-guardian.exe +0 -0
  10. package/dist/src/composition/index.js +7 -1
  11. package/dist/src/features/launch/initialize-profile.js +23 -2
  12. package/dist/src/features/launch/profiles.d.ts +1 -1
  13. package/dist/src/features/launch/profiles.js +2 -2
  14. package/dist/src/features/launch/runtime-selection.d.ts +13 -9
  15. package/dist/src/features/launch/runtime-selection.js +6 -6
  16. package/dist/src/features/owned-ui/settings-app.js +6 -4
  17. package/dist/src/foundation/pi-component-adapter/index.d.ts +1 -1
  18. package/dist/src/foundation/pi-component-adapter/index.js +1 -1
  19. package/dist/src/foundation/pi-component-adapter/upstream/theme/theme.d.ts +8 -0
  20. package/dist/src/foundation/pi-component-adapter/upstream/theme/theme.js +12 -0
  21. package/dist/src/foundation/pi-engine-adapter/adapter.d.ts +5 -0
  22. package/dist/src/foundation/pi-engine-adapter/adapter.js +7 -0
  23. package/dist/src/foundation/pi-engine-adapter/settings-integration.d.ts +0 -2
  24. package/dist/src/foundation/pi-engine-adapter/settings-integration.js +24 -37
  25. package/dist/src/foundation/release/bootstrap.js +2 -2
  26. package/dist/src/foundation/ui-components/list-view.js +4 -4
  27. package/dist/src/foundation/ui-components/theme.d.ts +6 -0
  28. package/dist/src/foundation/ui-components/theme.js +1 -0
  29. package/dist/src/product-identity.json +7 -7
  30. package/docs/architecture/process-guardian-provenance.md +5 -5
  31. package/docs/architecture/toolchain.md +3 -3
  32. package/docs/ci-release-runbook.md +10 -2
  33. package/docs/manual-terminal-colour-check.md +55 -0
  34. package/package.json +9 -7
  35. package/dist/native/win32-x64/a1-process-guardian.exe +0 -0
  36. package/dist/src/composition/transparent-runtime.d.ts +0 -6
  37. package/dist/src/composition/transparent-runtime.js +0 -19
  38. package/dist/src/foundation/transparent-terminal/command-resolution.d.ts +0 -13
  39. package/dist/src/foundation/transparent-terminal/command-resolution.js +0 -72
  40. package/dist/src/foundation/transparent-terminal/foreground-broker.d.ts +0 -24
  41. package/dist/src/foundation/transparent-terminal/foreground-broker.js +0 -30
  42. package/dist/src/foundation/transparent-terminal/index.d.ts +0 -4
  43. package/dist/src/foundation/transparent-terminal/index.js +0 -4
  44. package/dist/src/foundation/transparent-terminal/main.d.ts +0 -9
  45. package/dist/src/foundation/transparent-terminal/main.js +0 -87
  46. package/dist/src/foundation/transparent-terminal/native-launcher.d.ts +0 -25
  47. package/dist/src/foundation/transparent-terminal/native-launcher.js +0 -146
  48. /package/bin/{a1.js → cli.js} +0 -0
  49. /package/bin/{a1-supervisor.js → supervisor.js} +0 -0
package/README.md CHANGED
@@ -4,19 +4,12 @@ A1 is a terminal-native agent launcher for Windows, Linux, and macOS. Bare `a1`
4
4
 
5
5
  ## Install
6
6
 
7
- Requirements:
8
-
9
- - Node.js 22.19 through 24.x
10
- - npm 11
11
-
12
- Published packages include the integrity-verified process guardian for supported platforms. Building A1 from source additionally requires Rust/Cargo 1.85 or newer.
7
+ Requires Node.js 22.19 through 24.x and npm 11.
13
8
 
14
9
  ```sh
15
10
  npm install --global @timurproko/a1@latest
16
11
  ```
17
12
 
18
- The package installs only the `a1` command.
19
-
20
13
  ## Commands
21
14
 
22
15
  ```sh
@@ -28,79 +21,23 @@ a1 update # update to npm latest
28
21
  a1 update:next # update to npm next
29
22
  ```
30
23
 
31
- Bare `a1` owns its Pi-compatible TUI composition and uses the public Pi engine, components, and terminal APIs. It does not insert a PTY or terminal-byte relay. Default visuals and workflows track the pinned vanilla Pi baseline; A1-specific visual customization and structured tabs remain disabled.
32
-
33
- `a1 pi` is the untouched upstream fallback and comparison oracle. Use it to distinguish A1-owned UI problems from upstream Pi, profile, provider, or terminal problems. The `a1 ui` subcommand was removed and is not a compatibility alias; run bare `a1` for the owned UI.
24
+ The `a1 ui` subcommand was removed; run bare `a1` for the owned UI.
34
25
 
35
- `a1 sandbox` is unchanged: it launches vanilla Pi with isolated Pi configuration and resources. “Sandbox” does not mean operating-system, filesystem, process, network, or credential security isolation. See [`docs/features/launch-profiles.md`](docs/features/launch-profiles.md) for first-run directories, independent `/login`, trust behavior, extension placement, and recovery.
36
-
37
- Every interactive command creates an independent, non-detachable launch instance. Any number of `a1`, `a1 pi`, and `a1 sandbox` commands may run in separate terminals at the same time. Closing one command closes only its owned UI/Pi process and all agents, extensions, tools, daemons, and descendants created by that invocation; it never requires finding PIDs, deleting control state, or restarting the supervisor. The idle supervisor may remain available for release coordination after all interactive instances close, but it owns no surviving instance runtime.
26
+ Any number of `a1`, `a1 pi`, and `a1 sandbox` commands may run in separate terminals at the same time. Closing one command closes only its own UI/Pi process and everything it spawned.
38
27
 
39
28
  ## Develop
40
29
 
41
30
  ```sh
42
- npm ci
43
- npm run build
44
- npm start
45
- ```
46
-
47
- `npm start` gives each invocation isolated A1 development state and an isolated development Pi profile. Use `npm start -- --print-environment` to inspect the selected paths without launching the UI.
48
-
49
- A1 control state uses `%APPDATA%\\A1` and `%LOCALAPPDATA%\\A1` on Windows, and the `a1` directory under XDG config/data/runtime roots on Unix. Override it only with declared `A1_*` variables such as `A1_CONFIG_DIR`, `A1_DATA_DIR`, `A1_RUNTIME_DIR`, `A1_DATABASE_PATH`, and `A1_ENDPOINT`. Pi profile roots remain `~/.a1/agent`, `~/.pi/agent`, and `~/.a1/sandbox`. This is a no-migration identity hard cut; see [`docs/architecture/toolchain.md`](docs/architecture/toolchain.md#identity-hard-cut-and-cleanup) before removing obsolete control state.
50
-
51
- ### Worktree lifecycle
52
-
53
- The primary worktree stays on `develop` and is integration-only. Each task uses a detached worktree under the repository's ignored `.worktrees/` directory, based on current `origin/develop` unless another base is selected.
54
-
55
- ```sh
56
- git fetch origin --prune
57
- git worktree add --detach .worktrees/<task-id> origin/develop
58
- cd .worktrees/<task-id>
31
+ npm ci # install exact locked dependencies
32
+ npm run build # compile TypeScript and the process guardian into dist
33
+ npm start # build and launch an isolated development instance
34
+ npm run test:fast # typecheck + fast suite, no build needed
35
+ npm test # same as test:fast
36
+ npm run test:full # complete non-physical suite
59
37
  ```
60
38
 
61
- Commit and validate coherent work there. Because `develop` is protected, push the detached commit to one temporary remote branch and merge one pull request for the requested change. After merge, update the primary worktree and remove the task worktree only when its commits are reachable from `develop`:
39
+ ## Release
62
40
 
63
41
  ```sh
64
- git push origin HEAD:refs/heads/<task-id>
65
- gh pr create --base develop --head <task-id>
66
- # After the pull request is merged:
67
- cd <repository-root>
68
- git fetch origin --prune
69
- git merge --ff-only origin/develop
70
- git worktree remove .worktrees/<task-id>
71
- git worktree prune
42
+ npm run release:next # publish current develop tip to npm next via trusted CI workflows
72
43
  ```
73
-
74
- Manual acceptance does not leave a permanent validation checkout. Once acceptance is recorded, archive the completed OpenSpec change, integrate the archive commit into `develop`, then remove and prune every task or acceptance worktree retained for that change. Never remove a worktree before all of its implementation, acceptance, and archive commits are reachable from `develop`.
75
-
76
- Local package archives and ad hoc test builds belong under `.builds/`, never in the repository root. For manual package tests, use:
77
-
78
- ```sh
79
- mkdir -p .builds
80
- npm pack --ignore-scripts --pack-destination .builds
81
- ```
82
-
83
- Run the non-desktop gates with:
84
-
85
- ```sh
86
- npm run typecheck
87
- npm run check:architecture
88
- npm run check:deprecated
89
- npm test
90
- npm run test:release
91
- ```
92
-
93
- Automated physical-terminal interaction must never run on an active workstation. Future physical certification may run only on dedicated disposable workers or VMs with exclusive test desktops.
94
-
95
- ## Releases
96
-
97
- An exact manually accepted `-dev.N` candidate may publish under npm tag `next` after applicable non-desktop gates pass. Such a preview is explicitly uncertified, cannot move `latest`, and cannot claim stable terminal parity or platform support. Stable claims require deferred physical and cross-platform certification against the exact package.
98
-
99
- Trusted preview publication uses `.github/workflows/publish-next.yml` and npm provenance. Local release mechanics are documented in [`docs/architecture/toolchain.md`](docs/architecture/toolchain.md).
100
-
101
- ## Architecture
102
-
103
- - [`docs/architecture/boundaries.md`](docs/architecture/boundaries.md)
104
- - [`docs/architecture/toolchain.md`](docs/architecture/toolchain.md)
105
- - [`docs/manual-transparent-checkpoint.md`](docs/manual-transparent-checkpoint.md)
106
- - [`docs/features/launch-profiles.md`](docs/features/launch-profiles.md)
@@ -10,7 +10,7 @@ const profileId = process.env.A1_LAUNCH_PROFILE ?? "a1";
10
10
  runLaunchGuardian({
11
11
  profileId,
12
12
  releaseRoot,
13
- uiEntry: resolve(releaseRoot, "bin", "a1-ui.js"),
13
+ uiEntry: resolve(releaseRoot, "bin", "ui.js"),
14
14
  environment: process.env,
15
15
  cwd: process.cwd(),
16
16
  }).then(
@@ -3,21 +3,14 @@
3
3
  const { runSelectedInteractiveRuntime } = await import("../dist/src/features/launch/index.js");
4
4
 
5
5
  runSelectedInteractiveRuntime(process.env.A1_LAUNCH_PROFILE ?? "a1", {
6
- ownedUi: async profileId => {
6
+ ownedUi: async (profileId, ownedSurfaces) => {
7
7
  const [{ runOwnedUi }, { composeOwnedUi }] = await Promise.all([
8
8
  import("../dist/src/features/owned-ui/index.js"),
9
9
  import("../dist/src/composition/index.js"),
10
10
  ]);
11
- // A1_OWNED_SURFACES=off presents pinned Pi's interface through A1's own
12
- // rendering and input, which is what the parity comparison measures.
13
- const ownedSurfaces = process.env.A1_OWNED_SURFACES === "off" ? "off" : "on";
14
11
  const { application, settings } = await composeOwnedUi({ cwd: process.cwd(), profileId, ownedSurfaces });
15
12
  return await runOwnedUi({ application, ...(settings === null ? {} : { settings }) });
16
13
  },
17
- transparent: async profileId => {
18
- const { runSelectedTransparentRuntime } = await import("../dist/src/composition/transparent-runtime.js");
19
- return await runSelectedTransparentRuntime(profileId);
20
- },
21
14
  }).then(
22
15
  code => { process.exitCode = code; },
23
16
  error => {
@@ -5,10 +5,10 @@
5
5
  "platform": "darwin",
6
6
  "architecture": "arm64",
7
7
  "capability": "unsupported",
8
- "builtAt": "2026-08-22T13:11:56.818Z",
8
+ "builtAt": "2026-08-23T08:07:45.115Z",
9
9
  "artifact": {
10
- "filename": "a1-process-guardian",
11
- "sha256": "bb07d029b67a5eae4fbf3dea64f96cdf03462920949f2ea7e9e7b1bd33dbabc1",
10
+ "filename": "process-guardian",
11
+ "sha256": "7524bf568992517f50ed53196c861c5edeba0d7275633bb4735ab45bd5963a28",
12
12
  "size": 356368
13
13
  },
14
14
  "provenance": {
@@ -5,10 +5,10 @@
5
5
  "platform": "linux",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-08-22T13:12:07.615Z",
8
+ "builtAt": "2026-08-23T08:07:37.399Z",
9
9
  "artifact": {
10
- "filename": "a1-process-guardian",
11
- "sha256": "f66cc7ac6d30ad424bc00992e4c5425259d36f055893d872eda9cf518bdb2ed0",
10
+ "filename": "process-guardian",
11
+ "sha256": "29e22fe29de2828982bc67ef418c4adcaab1490281477b7bea592ec6fe621bcd",
12
12
  "size": 415696
13
13
  },
14
14
  "provenance": {
@@ -5,10 +5,10 @@
5
5
  "platform": "win32",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-08-22T13:12:29.999Z",
8
+ "builtAt": "2026-08-23T08:08:17.265Z",
9
9
  "artifact": {
10
- "filename": "a1-process-guardian.exe",
11
- "sha256": "83641a8a264dae32e57020ef1f37823ffe186b5acd28df18eae1afa80d25854c",
10
+ "filename": "process-guardian.exe",
11
+ "sha256": "a803cf8cc8bb6143d2adb7378183d4366a6bb880e110786ebeb7e778b64b6c59",
12
12
  "size": 172544
13
13
  },
14
14
  "provenance": {
@@ -5,7 +5,7 @@ import { assertPresentationComponent, assertPresentationRuntime, } from "../foun
5
5
  import { OwnedUiSessionShell } from "../foundation/pi-owned-ui-integration/index.js";
6
6
  import { OwnedUiSettingsSession, OwnedUiSettingsStore } from "../foundation/owned-ui-settings/index.js";
7
7
  import { UiAppHost, UiAppRegistry } from "../foundation/ui-apps/index.js";
8
- import { getAvailablePiThemes, piTheme } from "../foundation/pi-component-adapter/index.js";
8
+ import { applyConfiguredPiTheme, getAvailablePiThemes, piTheme } from "../foundation/pi-component-adapter/index.js";
9
9
  import { faint } from "../foundation/ui-components/index.js";
10
10
  import { SETTINGS_APP_ID, SETTINGS_ROUTE, SettingsApp } from "../features/owned-ui/index.js";
11
11
  import { resolveProductPaths } from "../foundation/lifecycle/index.js";
@@ -29,6 +29,9 @@ export async function composeOwnedUi(options = {}) {
29
29
  store: new OwnedUiSettingsStore({ configDir: resolveProductPaths().configDir, profileId: options.profileId }),
30
30
  agentProvider: () => adapter.settingsPort(),
31
31
  });
32
+ // Before anything is drawn, so every surface uses the configured theme rather
33
+ // than the one guessed from the terminal's background.
34
+ applyConfiguredPiTheme(adapter.configuredTheme());
32
35
  const routeHost = settings === null || options.ownedSurfaces === "off" ? null : createOwnedRouteHost(settings);
33
36
  const shell = new OwnedUiSessionShell({
34
37
  backend: adapter,
@@ -243,6 +246,9 @@ function pinnedTheme() {
243
246
  return {
244
247
  fg: (token, text) => piTheme().fg(token, text),
245
248
  bold: (text) => piTheme().bold(text),
249
+ // Unpainted, so a list reads against the terminal the reader actually has
250
+ // rather than against the background the configured theme was built for.
251
+ plain: (text) => text,
246
252
  // Quiet, and quieter again: the terminal's own faint attribute over the
247
253
  // dimmest colour the theme has.
248
254
  disabled: (text) => faint(piTheme().fg("dim", text)),
@@ -38,7 +38,7 @@ async function ensureSafeDirectoryChain(root) {
38
38
  current = parent;
39
39
  }
40
40
  for (const path of missing.reverse())
41
- await mkdir(path, { mode: 0o700 });
41
+ await createOwnedDirectory(path, "profile path");
42
42
  }
43
43
  async function ensureOwnedDirectory(path) {
44
44
  const metadata = await lstat(path).catch(error => {
@@ -51,7 +51,28 @@ async function ensureOwnedDirectory(path) {
51
51
  throw new Error(`profile resource path is not an owned directory: ${path}`);
52
52
  return;
53
53
  }
54
- await mkdir(path, { mode: 0o700 });
54
+ await createOwnedDirectory(path, "profile resource path");
55
+ }
56
+ /**
57
+ * Creates a directory, treating "it already exists" as another launch having won
58
+ * the race rather than as a failure — but still refusing what it finds if that is
59
+ * not an owned directory.
60
+ */
61
+ async function createOwnedDirectory(path, subject) {
62
+ try {
63
+ await mkdir(path, { mode: 0o700 });
64
+ return;
65
+ }
66
+ catch (error) {
67
+ if (!isAlreadyThere(error))
68
+ throw error;
69
+ }
70
+ const metadata = await lstat(path);
71
+ if (!metadata.isDirectory() || metadata.isSymbolicLink())
72
+ throw new Error(`${subject} is not an owned directory: ${path}`);
73
+ }
74
+ function isAlreadyThere(error) {
75
+ return typeof error === "object" && error !== null && error.code === "EEXIST";
55
76
  }
56
77
  function isMissing(error) {
57
78
  return error instanceof Error && "code" in error && error.code === "ENOENT";
@@ -7,6 +7,6 @@ export interface LaunchProfileContract {
7
7
  readonly productSurface: "agent" | "vanilla-baseline" | "isolated-profile";
8
8
  readonly configurationRootPolicy: ConfigurationRootPolicy;
9
9
  readonly projectTrust: ProjectTrustPolicy;
10
- readonly terminalCapability: "owned-ui" | "transparent";
10
+ readonly terminalCapability: "owned-ui";
11
11
  }
12
12
  export declare function launchProfileContract(id: LaunchProfileId): LaunchProfileContract;
@@ -11,14 +11,14 @@ const contracts = Object.freeze({
11
11
  productSurface: "vanilla-baseline",
12
12
  configurationRootPolicy: "pi-default",
13
13
  projectTrust: "pi-default",
14
- terminalCapability: "transparent",
14
+ terminalCapability: "owned-ui",
15
15
  }),
16
16
  sandbox: Object.freeze({
17
17
  id: "sandbox",
18
18
  productSurface: "isolated-profile",
19
19
  configurationRootPolicy: "sandbox-profile",
20
20
  projectTrust: "ignore",
21
- terminalCapability: "transparent",
21
+ terminalCapability: "owned-ui",
22
22
  }),
23
23
  });
24
24
  export function launchProfileContract(id) {
@@ -1,15 +1,19 @@
1
1
  import { type LaunchProfileId } from "../../foundation/lifecycle/index.js";
2
- export type TransparentInteractiveProfileId = Exclude<LaunchProfileId, "a1">;
3
- export type InteractiveRuntimeSelection = {
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.
8
+ */
9
+ export type OwnedUiProfileId = LaunchProfileId;
10
+ export interface InteractiveRuntimeSelection {
4
11
  readonly kind: "owned-ui";
5
- readonly profileId: Extract<LaunchProfileId, "a1">;
6
- } | {
7
- readonly kind: "transparent";
8
- readonly profileId: TransparentInteractiveProfileId;
9
- };
12
+ readonly profileId: OwnedUiProfileId;
13
+ readonly ownedSurfaces: "on" | "off";
14
+ }
10
15
  export interface InteractiveRuntimeRunners {
11
- readonly ownedUi: (profileId: Extract<LaunchProfileId, "a1">) => Promise<number>;
12
- readonly transparent: (profileId: TransparentInteractiveProfileId) => Promise<number>;
16
+ readonly ownedUi: (profileId: OwnedUiProfileId, ownedSurfaces: "on" | "off") => Promise<number>;
13
17
  }
14
18
  export declare function selectInteractiveRuntime(profileId: string): InteractiveRuntimeSelection;
15
19
  export declare function runSelectedInteractiveRuntime(profileId: string, runners: InteractiveRuntimeRunners): Promise<number>;
@@ -1,13 +1,13 @@
1
1
  import { assertLaunchProfileId } from "../../foundation/lifecycle/index.js";
2
2
  export function selectInteractiveRuntime(profileId) {
3
3
  assertLaunchProfileId(profileId);
4
- return profileId === "a1"
5
- ? Object.freeze({ kind: "owned-ui", profileId: "a1" })
6
- : Object.freeze({ kind: "transparent", profileId });
4
+ return Object.freeze({
5
+ kind: "owned-ui",
6
+ profileId,
7
+ ownedSurfaces: profileId === "a1" ? "on" : "off",
8
+ });
7
9
  }
8
10
  export async function runSelectedInteractiveRuntime(profileId, runners) {
9
11
  const selection = selectInteractiveRuntime(profileId);
10
- return selection.kind === "owned-ui"
11
- ? await runners.ownedUi(selection.profileId)
12
- : await runners.transparent(selection.profileId);
12
+ return await runners.ownedUi(selection.profileId, selection.ownedSurfaces);
13
13
  }
@@ -208,10 +208,12 @@ export class SettingsApp {
208
208
  const overRow = menuRowAt(frame, event.row - 1, event.column);
209
209
  const overMenu = overRow !== null;
210
210
  if (event.kind === "motion") {
211
- // While the menu is open it owns the pointer: the row under it lights up,
212
- // and nothing behind it hovers.
213
- const hadHover = this.#hoverKey !== null;
214
- this.#hoverKey = null;
211
+ // The menu owns the pointer, but the row it came from is still the thing
212
+ // being changed, so its value keeps reading as the one under the pointer.
213
+ // Only a dialog that takes the screen puts that out.
214
+ const hadHover = this.#hoverKey !== menu.anchorKey || this.#hoverRegion !== "value";
215
+ this.#hoverKey = menu.anchorKey;
216
+ this.#hoverRegion = "value";
215
217
  if (!overMenu) {
216
218
  const cleared = menu.index !== -1;
217
219
  menu.index = -1;
@@ -3,7 +3,7 @@ export * from "./conformance.js";
3
3
  export * from "./shell-components.js";
4
4
  export * from "./theme.js";
5
5
  export * from "./upstream/theme/theme-controller.js";
6
- export { getAvailablePiThemes } from "./upstream/theme/theme.js";
6
+ export { applyConfiguredPiTheme, getAvailablePiThemes } from "./upstream/theme/theme.js";
7
7
  export * from "./upstream/components/countdown-timer.js";
8
8
  export * from "./upstream/components/extension-editor.js";
9
9
  export * from "./upstream/components/session-selector.js";
@@ -3,7 +3,7 @@ export * from "./conformance.js";
3
3
  export * from "./shell-components.js";
4
4
  export * from "./theme.js";
5
5
  export * from "./upstream/theme/theme-controller.js";
6
- export { getAvailablePiThemes } from "./upstream/theme/theme.js";
6
+ export { applyConfiguredPiTheme, getAvailablePiThemes } from "./upstream/theme/theme.js";
7
7
  export * from "./upstream/components/countdown-timer.js";
8
8
  export * from "./upstream/components/extension-editor.js";
9
9
  export * from "./upstream/components/session-selector.js";
@@ -51,3 +51,11 @@ export declare function resolvePiThemeSetting(setting: string | undefined, termi
51
51
  export declare function detectPiTerminalBackgroundFromEnv(environment?: NodeJS.ProcessEnv): PiTerminalThemeDetection;
52
52
  export declare function detectPiTerminalBackgroundTheme(ui: PiTerminalThemeDetector, timeoutMs: number, environment?: NodeJS.ProcessEnv): Promise<PiTerminalThemeDetection>;
53
53
  export declare function detectPiTerminalThemeForAuto(ui: PiTerminalThemeDetector, timeoutMs: number, environment?: NodeJS.ProcessEnv): Promise<PiTerminalTheme>;
54
+ /**
55
+ * Applies the theme the reader configured, as the engine does at startup. The
56
+ * setting is either a theme's name or the `light/dark` pair meaning "follow the
57
+ * terminal", and only the pair consults the terminal's own background. An
58
+ * unreadable or absent setting falls back to that detection, which is what the
59
+ * engine does with an unset theme.
60
+ */
61
+ export declare function applyConfiguredPiTheme(setting: string | undefined): PiThemeResult;
@@ -312,3 +312,15 @@ function ansi256ToHex(index) {
312
312
  function isRecord(value) {
313
313
  return typeof value === "object" && value !== null && !Array.isArray(value);
314
314
  }
315
+ /**
316
+ * Applies the theme the reader configured, as the engine does at startup. The
317
+ * setting is either a theme's name or the `light/dark` pair meaning "follow the
318
+ * terminal", and only the pair consults the terminal's own background. An
319
+ * unreadable or absent setting falls back to that detection, which is what the
320
+ * engine does with an unset theme.
321
+ */
322
+ export function applyConfiguredPiTheme(setting) {
323
+ const detected = detectPiTerminalBackgroundFromEnv();
324
+ const resolved = resolvePiThemeSetting(setting, detected.theme);
325
+ return applyPiTheme(resolved ?? detected.theme);
326
+ }
@@ -113,6 +113,11 @@ export declare class PiEngineAdapter {
113
113
  unbindExtensionUi(): Promise<void>;
114
114
  workflowAutocompleteCommands(): readonly PiWorkflowAutocompleteCommand[];
115
115
  cycleModelWorkflow(direction: "forward" | "backward"): Promise<PiWorkflowResult>;
116
+ /**
117
+ * The theme the engine is configured with, in the engine's own grammar: a
118
+ * theme's name, or a `light/dark` pair meaning "follow the terminal".
119
+ */
120
+ configuredTheme(): string | undefined;
116
121
  /** Settings port for the live runtime, or null before the runtime is available. */
117
122
  settingsPort(): AgentSettingsPort | null;
118
123
  pinnedModelSelectorContext(): {
@@ -380,6 +380,13 @@ export class PiEngineAdapter {
380
380
  return workflowResult("model", "failed", error instanceof Error ? error.message : String(error));
381
381
  }
382
382
  }
383
+ /**
384
+ * The theme the engine is configured with, in the engine's own grammar: a
385
+ * theme's name, or a `light/dark` pair meaning "follow the terminal".
386
+ */
387
+ configuredTheme() {
388
+ return this.#runtime?.services.settingsManager.getThemeSetting();
389
+ }
383
390
  /** Settings port for the live runtime, or null before the runtime is available. */
384
391
  settingsPort() {
385
392
  const settings = this.#runtime?.services.settingsManager;
@@ -17,8 +17,6 @@ export interface PiSettingsProviders {
17
17
  * entry per terminal appearance.
18
18
  */
19
19
  export declare const AUTOMATIC_THEME = "automatic";
20
- export declare const LIGHT_THEME_KEY = "themeLight";
21
- export declare const DARK_THEME_KEY = "themeDark";
22
20
  interface AutomaticTheme {
23
21
  readonly light: string;
24
22
  readonly dark: string;
@@ -7,8 +7,9 @@ import piSettingsMetadata from "./pi-settings-metadata.json" with { type: "json"
7
7
  * entry per terminal appearance.
8
8
  */
9
9
  export const AUTOMATIC_THEME = "automatic";
10
- export const LIGHT_THEME_KEY = "themeLight";
11
- export const DARK_THEME_KEY = "themeDark";
10
+ /** The themes an automatic setting names for each terminal appearance. */
11
+ const LIGHT_APPEARANCE_THEME = "light";
12
+ const DARK_APPEARANCE_THEME = "dark";
12
13
  const THEME_KEY = "theme";
13
14
  const THINKING_KEY = "thinkingLevel";
14
15
  /** A stored theme is automatic when it names one theme per terminal appearance. */
@@ -47,10 +48,6 @@ export function settingsInventoryDrift(presented, mapped) {
47
48
  };
48
49
  }
49
50
  /** What the engine offers for a setting, as the strings it words them with. */
50
- /** One appearance of an automatic theme, offered as an ordinary setting. */
51
- function appearance(key, label, description, themes) {
52
- return { key, valueType: "enum", writable: true, choices: themes, label, description, resolvedWhenRead: true };
53
- }
54
51
  function offered(key) {
55
52
  return PRESENTATION.settings[key]?.values ?? [];
56
53
  }
@@ -95,15 +92,9 @@ export class PiSettingsIntegration {
95
92
  })
96
93
  .map(descriptor => this.#resolved(descriptor))
97
94
  .sort((left, right) => rank(left.key) - rank(right.key));
98
- const pair = parseAutomaticTheme(this.#storedTheme());
99
- if (pair === null)
100
- return listed;
101
- const themes = this.#themes();
102
- return [
103
- ...listed,
104
- appearance(LIGHT_THEME_KEY, "Light theme", "Theme to use in automatic mode when the terminal is light", themes),
105
- appearance(DARK_THEME_KEY, "Dark theme", "Theme to use in automatic mode when the terminal is dark", themes),
106
- ];
95
+ // Following the terminal is one choice on the theme itself, not two further
96
+ // settings: the pair behind it is the engine's grammar, not a reader's task.
97
+ return listed;
107
98
  }
108
99
  /** Overlays the values a setting can only offer once something is running. */
109
100
  #resolved(descriptor) {
@@ -129,20 +120,16 @@ export class PiSettingsIntegration {
129
120
  return typeof stored === "string" ? stored : "";
130
121
  }
131
122
  async readSetting(key) {
132
- if (key === THEME_KEY || key === LIGHT_THEME_KEY || key === DARK_THEME_KEY) {
123
+ if (key === THEME_KEY) {
133
124
  const pair = parseAutomaticTheme(this.#storedTheme());
134
- if (key === THEME_KEY)
135
- return pair === null ? this.#operations.get(key)?.read() : AUTOMATIC_THEME;
136
- if (pair === null)
137
- return undefined;
138
- return key === LIGHT_THEME_KEY ? pair.light : pair.dark;
125
+ return pair === null ? this.#operations.get(key)?.read() : AUTOMATIC_THEME;
139
126
  }
140
127
  return this.#operations.get(key)?.read();
141
128
  }
142
129
  async writeSetting(key, value) { this.writeSettingNow(key, value); }
143
130
  writeSettingNow(key, value) {
144
- if (typeof value === "string" && (key === THEME_KEY || key === LIGHT_THEME_KEY || key === DARK_THEME_KEY)) {
145
- this.#writeTheme(key, value);
131
+ if (typeof value === "string" && key === THEME_KEY) {
132
+ this.#writeTheme(value);
146
133
  return;
147
134
  }
148
135
  const operation = this.#operations.get(key);
@@ -151,26 +138,26 @@ export class PiSettingsIntegration {
151
138
  operation.write(value);
152
139
  }
153
140
  /**
154
- * Composes the stored theme from the part that changed. Turning automatic on
155
- * starts from the theme already in use for both appearances, which is where
156
- * the engine's own editor starts too.
141
+ * Writes the theme in the engine's own grammar. Following the terminal is
142
+ * stored as the pair the engine reads for it, made of the themes named for
143
+ * each appearance when they are installed; where they are not, the theme
144
+ * already in use stands for both, so the setting still round-trips.
157
145
  */
158
- #writeTheme(key, value) {
146
+ #writeTheme(value) {
159
147
  const theme = this.#operations.get(THEME_KEY);
160
148
  if (!theme)
161
- throw new Error(`setting is unavailable: ${key}`);
162
- const current = this.#storedTheme();
163
- const pair = parseAutomaticTheme(current);
164
- if (key === THEME_KEY) {
165
- if (value !== AUTOMATIC_THEME)
166
- theme.write(value);
167
- else if (pair === null)
168
- theme.write(`${current}/${current}`);
149
+ throw new Error(`setting is unavailable: ${THEME_KEY}`);
150
+ if (value !== AUTOMATIC_THEME) {
151
+ theme.write(value);
169
152
  return;
170
153
  }
171
- if (pair === null)
154
+ if (parseAutomaticTheme(this.#storedTheme()) !== null)
172
155
  return;
173
- theme.write(key === LIGHT_THEME_KEY ? `${value}/${pair.dark}` : `${pair.light}/${value}`);
156
+ const installed = this.#themes();
157
+ const current = this.#storedTheme();
158
+ const light = installed.includes(LIGHT_APPEARANCE_THEME) ? LIGHT_APPEARANCE_THEME : current;
159
+ const dark = installed.includes(DARK_APPEARANCE_THEME) ? DARK_APPEARANCE_THEME : current;
160
+ theme.write(`${light}/${dark}`);
174
161
  }
175
162
  async flush() { await this.settings.flush(); }
176
163
  }
@@ -138,7 +138,7 @@ export async function certifyMaterializedRelease(release, dataDir, verification
138
138
  return path;
139
139
  }
140
140
  export async function startSupervisor(release, environment) {
141
- const entry = await resolveReleaseEntryPoint(release, "bin/a1-supervisor.js");
141
+ const entry = await resolveReleaseEntryPoint(release, "bin/supervisor.js");
142
142
  const child = spawn(process.execPath, [entry], {
143
143
  detached: true,
144
144
  env: releaseEnvironment(environment, release),
@@ -152,7 +152,7 @@ export async function startSupervisor(release, environment) {
152
152
  child.unref();
153
153
  }
154
154
  async function launchUi(release, environment) {
155
- const entry = await resolveReleaseEntryPoint(release, "bin/a1-guardian.js");
155
+ const entry = await resolveReleaseEntryPoint(release, "bin/guardian.js");
156
156
  return await new Promise((resolvePromise, rejectPromise) => {
157
157
  const child = spawn(process.execPath, [entry], {
158
158
  env: releaseEnvironment(environment, release),
@@ -23,21 +23,21 @@ export function renderGroupHeader(title, width, theme) {
23
23
  export function renderListRow(row, state, valueColumn, width, theme) {
24
24
  const cursor = state.selected ? "→ " : " ";
25
25
  const leftRaw = `${cursor} ${row.label}`;
26
- const left = `${theme.fg("accent", cursor)} ${theme.fg(state.selected ? "accent" : "text", row.label)}`;
26
+ const left = `${theme.fg("accent", cursor)} ${state.selected ? theme.fg("accent", row.label) : theme.plain(row.label)}`;
27
27
  const gap = Math.max(2, valueColumn - displayWidth(leftRaw));
28
28
  // Pointing anywhere on the row is pointing at the item; pointing at the value
29
29
  // is what brightens it. The selection speaks through the label alone.
30
30
  const valueHovered = state.hovered && state.region !== "label";
31
31
  const stepper = row.stepper !== undefined && valueHovered;
32
- const value = theme.fg(valueHovered ? "text" : "muted", row.value);
32
+ const value = valueHovered ? theme.plain(row.value) : theme.fg("muted", row.value);
33
33
  const minus = stepper
34
34
  ? row.stepper?.lower === true
35
- ? theme.fg(state.region === "minus" ? "text" : "dim", "- ")
35
+ ? state.region === "minus" ? theme.plain("- ") : theme.fg("dim", "- ")
36
36
  : theme.disabled("- ")
37
37
  : "";
38
38
  const plus = stepper
39
39
  ? row.stepper?.raise === true
40
- ? theme.fg(state.region === "plus" ? "text" : "dim", " +")
40
+ ? state.region === "plus" ? theme.plain(" +") : theme.fg("dim", " +")
41
41
  : theme.disabled(" +")
42
42
  : "";
43
43
  const indent = Math.max(2, stepper ? gap - STEPPER_RESERVE : gap);
@@ -6,6 +6,12 @@ export type UiThemeToken = "accent" | "text" | "muted" | "dim" | "border" | "err
6
6
  export interface UiTheme {
7
7
  fg(token: UiThemeToken, text: string): string;
8
8
  bold(text: string): string;
9
+ /**
10
+ * The terminal's own foreground, left unpainted. What a list's own text is:
11
+ * a theme built for a light background would otherwise write near-black rows
12
+ * into a dark terminal, which is how the engine leaves its lists too.
13
+ */
14
+ plain(text: string): string;
9
15
  /** Inverted styling for the active row of a floating surface. */
10
16
  highlight(text: string): string;
11
17
  /** A control that is present but cannot act: quieter than quiet text. */
@@ -6,6 +6,7 @@
6
6
  export const PLAIN_THEME = Object.freeze({
7
7
  fg: (_token, text) => text,
8
8
  bold: (text) => text,
9
+ plain: (text) => text,
9
10
  highlight: (text) => text,
10
11
  disabled: (text) => text,
11
12
  panel: (text) => text,