@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.
- package/README.md +11 -74
- package/bin/{a1-guardian.js → guardian.js} +1 -1
- package/bin/{a1-ui.js → ui.js} +1 -8
- package/dist/native/darwin-arm64/manifest.json +3 -3
- package/dist/native/darwin-arm64/{a1-process-guardian → process-guardian} +0 -0
- package/dist/native/linux-x64/manifest.json +3 -3
- package/dist/native/linux-x64/{a1-process-guardian → process-guardian} +0 -0
- package/dist/native/win32-x64/manifest.json +3 -3
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/src/composition/index.js +7 -1
- package/dist/src/features/launch/initialize-profile.js +23 -2
- package/dist/src/features/launch/profiles.d.ts +1 -1
- package/dist/src/features/launch/profiles.js +2 -2
- package/dist/src/features/launch/runtime-selection.d.ts +13 -9
- package/dist/src/features/launch/runtime-selection.js +6 -6
- package/dist/src/features/owned-ui/settings-app.js +6 -4
- package/dist/src/foundation/pi-component-adapter/index.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/index.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/theme/theme.d.ts +8 -0
- package/dist/src/foundation/pi-component-adapter/upstream/theme/theme.js +12 -0
- package/dist/src/foundation/pi-engine-adapter/adapter.d.ts +5 -0
- package/dist/src/foundation/pi-engine-adapter/adapter.js +7 -0
- package/dist/src/foundation/pi-engine-adapter/settings-integration.d.ts +0 -2
- package/dist/src/foundation/pi-engine-adapter/settings-integration.js +24 -37
- package/dist/src/foundation/release/bootstrap.js +2 -2
- package/dist/src/foundation/ui-components/list-view.js +4 -4
- package/dist/src/foundation/ui-components/theme.d.ts +6 -0
- package/dist/src/foundation/ui-components/theme.js +1 -0
- package/dist/src/product-identity.json +7 -7
- package/docs/architecture/process-guardian-provenance.md +5 -5
- package/docs/architecture/toolchain.md +3 -3
- package/docs/ci-release-runbook.md +10 -2
- package/docs/manual-terminal-colour-check.md +55 -0
- package/package.json +9 -7
- package/dist/native/win32-x64/a1-process-guardian.exe +0 -0
- package/dist/src/composition/transparent-runtime.d.ts +0 -6
- package/dist/src/composition/transparent-runtime.js +0 -19
- package/dist/src/foundation/transparent-terminal/command-resolution.d.ts +0 -13
- package/dist/src/foundation/transparent-terminal/command-resolution.js +0 -72
- package/dist/src/foundation/transparent-terminal/foreground-broker.d.ts +0 -24
- package/dist/src/foundation/transparent-terminal/foreground-broker.js +0 -30
- package/dist/src/foundation/transparent-terminal/index.d.ts +0 -4
- package/dist/src/foundation/transparent-terminal/index.js +0 -4
- package/dist/src/foundation/transparent-terminal/main.d.ts +0 -9
- package/dist/src/foundation/transparent-terminal/main.js +0 -87
- package/dist/src/foundation/transparent-terminal/native-launcher.d.ts +0 -25
- package/dist/src/foundation/transparent-terminal/native-launcher.js +0 -146
- /package/bin/{a1.js → cli.js} +0 -0
- /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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
39
|
+
## Release
|
|
62
40
|
|
|
63
41
|
```sh
|
|
64
|
-
|
|
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", "
|
|
13
|
+
uiEntry: resolve(releaseRoot, "bin", "ui.js"),
|
|
14
14
|
environment: process.env,
|
|
15
15
|
cwd: process.cwd(),
|
|
16
16
|
}).then(
|
package/bin/{a1-ui.js → ui.js}
RENAMED
|
@@ -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-
|
|
8
|
+
"builtAt": "2026-08-23T08:07:45.115Z",
|
|
9
9
|
"artifact": {
|
|
10
|
-
"filename": "
|
|
11
|
-
"sha256": "
|
|
10
|
+
"filename": "process-guardian",
|
|
11
|
+
"sha256": "7524bf568992517f50ed53196c861c5edeba0d7275633bb4735ab45bd5963a28",
|
|
12
12
|
"size": 356368
|
|
13
13
|
},
|
|
14
14
|
"provenance": {
|
|
Binary file
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
"platform": "linux",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-08-
|
|
8
|
+
"builtAt": "2026-08-23T08:07:37.399Z",
|
|
9
9
|
"artifact": {
|
|
10
|
-
"filename": "
|
|
11
|
-
"sha256": "
|
|
10
|
+
"filename": "process-guardian",
|
|
11
|
+
"sha256": "29e22fe29de2828982bc67ef418c4adcaab1490281477b7bea592ec6fe621bcd",
|
|
12
12
|
"size": 415696
|
|
13
13
|
},
|
|
14
14
|
"provenance": {
|
|
Binary file
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
"platform": "win32",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-08-
|
|
8
|
+
"builtAt": "2026-08-23T08:08:17.265Z",
|
|
9
9
|
"artifact": {
|
|
10
|
-
"filename": "
|
|
11
|
-
"sha256": "
|
|
10
|
+
"filename": "process-guardian.exe",
|
|
11
|
+
"sha256": "a803cf8cc8bb6143d2adb7378183d4366a6bb880e110786ebeb7e778b64b6c59",
|
|
12
12
|
"size": 172544
|
|
13
13
|
},
|
|
14
14
|
"provenance": {
|
|
Binary file
|
|
@@ -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
|
|
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
|
|
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"
|
|
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: "
|
|
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: "
|
|
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
|
-
|
|
3
|
-
|
|
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:
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
readonly profileId: TransparentInteractiveProfileId;
|
|
9
|
-
};
|
|
12
|
+
readonly profileId: OwnedUiProfileId;
|
|
13
|
+
readonly ownedSurfaces: "on" | "off";
|
|
14
|
+
}
|
|
10
15
|
export interface InteractiveRuntimeRunners {
|
|
11
|
-
readonly ownedUi: (profileId:
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
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.
|
|
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
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
this.#hoverKey
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
123
|
+
if (key === THEME_KEY) {
|
|
133
124
|
const pair = parseAutomaticTheme(this.#storedTheme());
|
|
134
|
-
|
|
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" &&
|
|
145
|
-
this.#writeTheme(
|
|
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
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
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(
|
|
146
|
+
#writeTheme(value) {
|
|
159
147
|
const theme = this.#operations.get(THEME_KEY);
|
|
160
148
|
if (!theme)
|
|
161
|
-
throw new Error(`setting is unavailable: ${
|
|
162
|
-
|
|
163
|
-
|
|
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 (
|
|
154
|
+
if (parseAutomaticTheme(this.#storedTheme()) !== null)
|
|
172
155
|
return;
|
|
173
|
-
|
|
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/
|
|
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/
|
|
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)} ${
|
|
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 =
|
|
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
|
-
?
|
|
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
|
-
?
|
|
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. */
|