@estebanforge/pi-antigravity-bridge 1.4.10 → 1.5.0
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/CHANGELOG.md +18 -0
- package/README.md +11 -59
- package/docs/APPROVAL-GATE.md +33 -0
- package/docs/ARCHITECTURE.md +1 -0
- package/docs/DEVELOPMENT.md +1 -0
- package/docs/ENGINES.md +46 -0
- package/docs/TODO.md +1 -1
- package/extensions/index.ts +145 -5
- package/package.json +1 -1
- package/src/acp/driver.ts +58 -1
- package/src/acp/usage-estimate.ts +59 -0
- package/src/config.ts +19 -1
- package/src/engine-picker.ts +155 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.5.0] - 2026-09-08
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- First-run engine picker. The choice of engine is left to the user: on a fresh install (no `config.json`, no `AGY_ENGINE`), the first interactive pi start opens a modal that explains both engines - stream-json needs the `agy` CLI installed and authenticated; ACP needs a second Google sign-in plus a ~1.5 GB server binary downloaded from Google. stream-json is preselected as the default; `esc` defers (nothing is written, the modal reappears next start). Headless sessions and existing installs are never asked.
|
|
10
|
+
- The ACP pick chains setup immediately instead of waiting for a restart: the ~1.5 GB server binary downloads right away (live percent in the status bar, phase milestones as chat lines), the Google sign-in opens when it lands, and a restart applies the engine. The next start's self-heal sees binary + auth settled and stays silent.
|
|
11
|
+
- `/agy engine` with no arguments (TUI) opens the same picker modal with identical semantics: plan mode blocks acp, an acp pick chains the download + sign-in, same-engine picks ack, esc acknowledges. Direct `/agy engine acp|stream-json` and the headless usage line are unchanged.
|
|
12
|
+
- Missing-CLI warning: while the stream-json engine is active and the `agy` binary cannot be found (PATH or `AGY_BIN`), every pi start warns with the official install URL (toast in the TUI, stderr headless) until the binary is detected. Auth state is not checked, presence only.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- The picker's ACP line no longer claims "adds image input and native diffs": tool-result images ride both engines (probe-verified 2026-09-07) and stream-json has its own diff rendering. A negative test pin keeps the claim from returning.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- README streamlined: the engine capability table, switching, and setup/auth details moved to docs/ENGINES.md; the approval-gate mechanics, configuration, and sample extension moved to docs/APPROVAL-GATE.md. Both stay linked from the README, which now carries the quick surface and states that engine choice is the user's.
|
|
21
|
+
- Dependencies: fast-uri 3.1.5 → 3.1.7 clears four high-severity advisories (repeated hostname percent-decoding SSRF, malformed IPv6 normalization SSRF, percent-encoded scheme and IDN host confusion; in-range lockfile bump under ajv's `^3.0.1`), qs 6.15.3 → 6.16.0 clears a moderate pair (via express, dev surface). npm audit reports zero.
|
|
22
|
+
|
|
5
23
|
## [1.4.10] - 2026-09-07
|
|
6
24
|
|
|
7
25
|
### Added
|
package/README.md
CHANGED
|
@@ -21,35 +21,13 @@ Multi-turn works. The provider binds a pi session to an agy conversation id (per
|
|
|
21
21
|
Turns run through one of two engines behind the same provider surface (`config.engine`, default `stream-json`):
|
|
22
22
|
|
|
23
23
|
- **stream-json** (default): the persistent `agy` CLI process. The tested default; live token usage; conversation resume via `--conversation`.
|
|
24
|
-
- **acp** (beta): Google's official ACP server (`agy_acp_server.par`), JSON-RPC 2.0 over stdio. Beta: parity-verified live against the current build (RC01) - text streaming, multi-turn resume via `session/load`, bridge tools, effort switching, serialization, abort recovery (see `scripts/parity-live.mjs`). Two known RC01 gaps remain: no usage fields (token display shows
|
|
24
|
+
- **acp** (beta): Google's official ACP server (`agy_acp_server.par`), JSON-RPC 2.0 over stdio. Beta: parity-verified live against the current build (RC01) - text streaming, multi-turn resume via `session/load`, bridge tools, effort switching, serialization, abort recovery (see `scripts/parity-live.mjs`). Two known RC01 gaps remain: no usage fields (token display shows client-side ESTIMATES until Google ships usage; `acp.usageEstimate` off to keep zeros) and no cancel (abort tears the server down and reloads it next turn).
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
The choice of engine is left to the user, with the trade-offs explained in the tool: a first-run picker modal asks once on a fresh install (stream-json preselected; `esc` defers, and the modal reappears next start), and `/agy engine` with no arguments reopens it anytime. An `acp` pick downloads the ~1.5 GB server binary and starts the Google sign-in immediately; a restart applies the engine. With stream-json active and the `agy` binary missing, pi warns on every start until the binary is found.
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
| Live token usage | Yes (live metrics from CLI step events) | No (absent in RC01, displays zero tokens) |
|
|
32
|
-
| Image prompt input | No (CLI prompt is text-only; images dropped) | Yes (native image blocks forwarded to server) |
|
|
33
|
-
| Image tool results | Yes (bridge tool results carry pixels; probe-verified 2026-09-07) | Yes (probe-verified 2026-09-05) |
|
|
34
|
-
| Audio prompt input | No (dropped) | Protocol advertised (`promptCapabilities.audio: true`) |
|
|
35
|
-
| Review-only plan mode | Yes (`--mode plan` review-only via `/agy mode plan`) | No (RC01 modes are permission levels; plan mode refused) |
|
|
36
|
-
| Leading slash commands in prompt | Disabled via `--disable-slash-commands` (sent as plain text) | Server intercepts recognized commands (e.g. `/plan`) and executes them under the active policy |
|
|
37
|
-
| Dynamic model / effort switch | Recycles process on model or effort change | Dynamic per-turn via `session/set_config_option` (no restart) |
|
|
38
|
-
| Process lifecycle | 1 persistent `agy` process per provider; recycles on drift | 1 persistent server process hosting N sessions concurrently |
|
|
39
|
-
| Session resume & persistence | Client-side map in `sessions.json` via `--conversation <id>` | Server-side session store via `session/load` and `session/new` |
|
|
40
|
-
| Turn cancel / abort | Kills process group; in-flight turn terminates | Teardown, kill, and auto-reload on RC01 (-32601 fallback) |
|
|
41
|
-
| MCP tool bridge routing | Injected filesystem config via `--add-dir` | Direct `mcpServers` param in `session/new` and `session/load` |
|
|
42
|
-
| Tool execution & visibility | Native re-exec (read-only) + wrapper replay (mutating) | Server executes tools natively; events stream with content |
|
|
43
|
-
| Inline file edit diffs | Sourced from git working tree in thinking block | Sourced from `tool_call content[]` or disk vs git HEAD |
|
|
44
|
-
| Permission handling | `--dangerously-skip-permissions` (unattended CLI requirement) | Protocol-native `session/request_permission` (auto-approve when `skipPermissions` is on; auto-deny when off) |
|
|
45
|
-
| Context digest delivery (G1) | Prepend plain text inline in prompt | Native `embeddedContext` resource block |
|
|
46
|
-
| System prompt delivery (G10) | Prepend to first prompt of conversation | Prepend to first prompt of conversation |
|
|
47
|
-
| Authentication methods | Inherits existing `agy` CLI OAuth state | 4 methods: `oauth-personal`, `oauth-business`, `gemini-api-key`, `agent-platform` |
|
|
48
|
-
| Wire protocol | Undocumented CLI NDJSON stream format | Versioned JSON-RPC 2.0 over stdio (`protocolVersion: 1`) |
|
|
49
|
-
| Diagnostics (`/agy doctor`) | Child PID, state, process spawns, recycles, queue stats | Server version, agentInfo, session counts, reconnect count, cancel support |
|
|
50
|
-
| Integration channel | Spawns internal CLI stream-json dialect | Official Google first-party ACP server binary |
|
|
51
|
-
|
|
52
|
-
Switch with `/agy engine acp|stream-json` (takes effect on restart). Setup is automatic: switching to `acp` installs Google's official ACP server binary from the [antigravity-acp registry entry](https://github.com/agentclientprotocol/registry) (`~/.local/opt/agy-acp/<build>/` + a `current` symlink, zip sha256 recorded; layout and pinning in [docs/ACP-ADOPTION-PLAN.md](docs/ACP-ADOPTION-PLAN.md)) and prepares the login. The login is your Antigravity subscription: the same account and plan you use for the Antigravity CLI (`agy`). Sign in explicitly with `/agy auth` (engine `acp` selected): it opens the Google login in your browser and completes when you finish it. If no browser is available (an SSH session on a remote machine), pi shows the sign-in URL to copy, plus the ssh port-forward command for the login redirect. It is no different from logging into the CLI; the server just keeps its own token file on your machine, like any Google tool, and this extension never sees your credentials. If you also export `GEMINI_API_KEY`, it is ignored: the server uses the auth type in settings.json, and setup always writes `oauth-personal`. A session start self-heals the same way, silently when everything is ready. Manual instructions (`/agy auth-manual`) surface only when a step fails. Sessions are engine-scoped, so switching engines never crosses conversations.
|
|
28
|
+
Full capability comparison, switching, and setup/auth details: [docs/ENGINES.md](docs/ENGINES.md).
|
|
29
|
+
|
|
30
|
+
Switch and setup details live in [docs/ENGINES.md](docs/ENGINES.md): switching to `acp` self-installs Google's official server binary from the [antigravity-acp registry entry](https://github.com/agentclientprotocol/registry) and prepares the login (your Antigravity subscription, same account as the `agy` CLI; the extension never sees your credentials). `/agy auth` signs in explicitly, `/agy doctor` diagnoses, and a session start self-heals silently when everything is ready. Sessions are engine-scoped, so switching engines never crosses conversations.
|
|
53
31
|
|
|
54
32
|
## What it cannot do
|
|
55
33
|
|
|
@@ -58,7 +36,7 @@ agy runs its own closed tool loop (`read_file`, `write_file`, `edit_file`, `run_
|
|
|
58
36
|
Residual limits (with or without the bridge):
|
|
59
37
|
|
|
60
38
|
- agy's own edits still land directly on disk; pi's inline diff review does not engage for them.
|
|
61
|
-
- agy commands run without per-action approval by default, same as every other tool in pi. The [Approval gate](
|
|
39
|
+
- agy commands run without per-action approval by default, same as every other tool in pi. The [Approval gate](docs/APPROVAL-GATE.md) can put pi-side review in front of agy's mutating native tools (off by default; `auto` enables it only when a pi permission extension is installed). See also [Permissions](#permissions) below.
|
|
62
40
|
- No cost accounting: cost stays zero because agy runs on your subscription quota. Token usage is live.
|
|
63
41
|
|
|
64
42
|
## MCP tool bridge (agy uses pi's tools)
|
|
@@ -94,35 +72,9 @@ bridge port, and the last lifecycle events without spending tokens.
|
|
|
94
72
|
|
|
95
73
|
## Approval gate (agy native tools)
|
|
96
74
|
|
|
97
|
-
agy
|
|
98
|
-
|
|
99
|
-
Mechanics: the extension stages an `.agents/hooks.json` group in the workspace; the Antigravity server/CLI fires a `PreToolUse` hook before each mutating native tool runs. The hook script (generated, per-pid, mode 0600 because it embeds the bridge token) POSTs the call to the bridge and polls for a decision. The bridge parks it, the provider interrupts the pi-side view of the agy turn with a `toolUse` for a SHADOW tool named `bash`/`write`/`edit` (same schema as the real builtin plus internal `__agy*` marker fields), and pi's whole `tool_call` surface applies: any installed permission extension gates the call unchanged, and only if nothing blocks does the fallback policy run. The decision travels back to the hook and agy enforces it; on deny the reason text is what agy's model sees. Marker calls never execute locally (a ticket check denies forged ones); non-marker calls delegate to a factory twin of the real builtin, so normal pi bash/write/edit behavior is unchanged. Read-only agy tools stay ungated. Every decision lands in the daily log with tool names, source, and latency.
|
|
75
|
+
agy runs its own agent loop with native tools (`run_command`, `create_file`, `edit_file`, ...), which pi's permission extensions never saw. The optional approval gate routes those calls through a pi-side approval: a staged PreToolUse hook parks the call, the provider surfaces it as a shadow `bash`/`write`/`edit` toolUse, and your permission extension (or the built-in ask/allow/deny fallback) decides before agy executes it. Off by default (`approvals.gateMode: auto` enables it only when a pi permission extension is detected); denials fail closed; read-only agy tools stay ungated.
|
|
100
76
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
```jsonc
|
|
104
|
-
{
|
|
105
|
-
"approvals": {
|
|
106
|
-
"gateMode": "auto", // auto | shadow | dedicated | off
|
|
107
|
-
"mode": "ask" // ask | allow | deny (fallback when no extension gates)
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
// env: AGY_APPROVALS=shadow AGY_APPROVALS_MODE=ask
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
`auto` (default) keeps the gate OFF until one of the known pi permission packages is detected (pi settings `packages` name-match or known config markers). `shadow` forces it on; `off` forces it off. `dedicated` currently stages the same shadow tools (the explicit `antigravity_approve` variant is planned; the config value is accepted today so the schema is stable). The fallback `mode` is consulted only when no extension blocked the call: `ask` shows a pi confirm dialog (headless runs deny, fail-closed), `allow` approves, `deny` blocks. Timeouts deny fail-closed: the staged hook timeout always exceeds the park budget, but a hook that outlives its timeout soft-passes upstream (verified against the ACP server), so the park always answers first.
|
|
114
|
-
|
|
115
|
-
If you write your own gate extension, it sees a normal pi tool call:
|
|
116
|
-
|
|
117
|
-
```typescript
|
|
118
|
-
export default function (pi) {
|
|
119
|
-
pi.on("tool_call", async (event) => {
|
|
120
|
-
if (event.input?.__agyGate && event.input.command?.startsWith("rm ")) {
|
|
121
|
-
return { block: true, reason: "rm is not allowed through the agy gate" };
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
```
|
|
77
|
+
Full mechanics, configuration, and a sample gate extension: [docs/APPROVAL-GATE.md](docs/APPROVAL-GATE.md).
|
|
126
78
|
|
|
127
79
|
## Install
|
|
128
80
|
|
|
@@ -134,7 +86,7 @@ Install with pi's package manager:
|
|
|
134
86
|
pi install npm:@estebanforge/pi-antigravity-bridge
|
|
135
87
|
```
|
|
136
88
|
|
|
137
|
-
Requires the **`agy` CLI** installed and authenticated. If you don't have it, follow Google's [official install guide](https://antigravity.google/docs/cli/install) for your platform, then run `agy` once to complete Google OAuth. The extension resolves `agy` on `$PATH`, or via the `AGY_BIN` environment variable.
|
|
89
|
+
Requires the **`agy` CLI** installed and authenticated. If you don't have it, follow Google's [official install guide](https://antigravity.google/docs/cli/install) for your platform, then run `agy` once to complete Google OAuth. The extension resolves `agy` on `$PATH`, or via the `AGY_BIN` environment variable. While the stream-json engine is active and the binary cannot be found, pi warns on every start (toast in the TUI, stderr headless) pointing at the install guide; the warning stops once the binary is detected.
|
|
138
90
|
|
|
139
91
|
## Usage
|
|
140
92
|
|
|
@@ -190,7 +142,7 @@ The `activate_skill` catalog mirrors pi's directory-based skill discovery: the t
|
|
|
190
142
|
/agy system-prompt on|off send pi's system prompt + AGENTS.md + the Pi Bridge tool-priority note to new agy conversations (default on)
|
|
191
143
|
/agy bridge all|mcp|none which pi tools the MCP bridge exposes to agy (default all; none = bridge off)
|
|
192
144
|
/agy acp-bin <path|auto> point the ACP engine at a specific server binary (auto = setup installs, or AGY_ACP_BIN; applies on the next ACP turn)
|
|
193
|
-
/agy engine acp|stream-json switch the turn engine (restart to apply; default stream-json; acp is beta and runs self-service setup: binary install + auth bootstrap)
|
|
145
|
+
/agy engine acp|stream-json switch the turn engine (restart to apply; default stream-json; acp is beta and runs self-service setup: binary install + auth bootstrap). No arguments opens the engine picker modal (TUI)
|
|
194
146
|
/agy auth-manual manual ACP credential setup (fallback; auto-setup normally covers this; default login = your Antigravity subscription, same account as the agy CLI)
|
|
195
147
|
/agy patch-cleanup restore the original pi files if an older version patched them
|
|
196
148
|
/agy clear drop all session bindings (force fresh conversations)
|
|
@@ -204,7 +156,7 @@ Because agy runs non-interactively under this provider (nothing can answer a `y/
|
|
|
204
156
|
|
|
205
157
|
If you want agy to execute nothing, use `/agy mode plan`. Do not combine `--sandbox` with skip-permissions ([#36](https://github.com/google-antigravity/antigravity-cli/issues/36)).
|
|
206
158
|
|
|
207
|
-
For per-action review of agy's mutating native tools (`run_command`, `create_file`, `edit_file`, ...), see the [Approval gate](
|
|
159
|
+
For per-action review of agy's mutating native tools (`run_command`, `create_file`, `edit_file`, ...), see the [Approval gate](docs/APPROVAL-GATE.md): with it on, the call must pass a pi-side approval (your permission extension, or the built-in ask/allow/deny fallback) before agy executes it.
|
|
208
160
|
|
|
209
161
|
### Run pi inside a sandbox
|
|
210
162
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Approval gate (agy native tools)
|
|
2
|
+
|
|
3
|
+
agy is not a plain model: it runs its own agent loop with its own native tools (`run_command`, `create_file`, `edit_file`, ...). Those calls execute inside agy with no pi involvement, so pi's permission extensions never saw them. The approval gate closes that gap: agy native tool calls pass through a pi-side approval in a form the existing permission-extension ecosystem gates with zero changes.
|
|
4
|
+
|
|
5
|
+
Mechanics: the extension stages an `.agents/hooks.json` group in the workspace; the Antigravity server/CLI fires a `PreToolUse` hook before each mutating native tool runs. The hook script (generated, per-pid, mode 0600 because it embeds the bridge token) POSTs the call to the bridge and polls for a decision. The bridge parks it, the provider interrupts the pi-side view of the agy turn with a `toolUse` for a SHADOW tool named `bash`/`write`/`edit` (same schema as the real builtin plus internal `__agy*` marker fields), and pi's whole `tool_call` surface applies: any installed permission extension gates the call unchanged, and only if nothing blocks does the fallback policy run. The decision travels back to the hook and agy enforces it; on deny the reason text is what agy's model sees. Marker calls never execute locally (a ticket check denies forged ones); non-marker calls delegate to a factory twin of the real builtin, so normal pi bash/write/edit behavior is unchanged. Read-only agy tools stay ungated. Every decision lands in the daily log with tool names, source, and latency.
|
|
6
|
+
|
|
7
|
+
Configuration (`/agy` config keys or environment):
|
|
8
|
+
|
|
9
|
+
```jsonc
|
|
10
|
+
{
|
|
11
|
+
"approvals": {
|
|
12
|
+
"gateMode": "auto", // auto | shadow | dedicated | off
|
|
13
|
+
"mode": "ask" // ask | allow | deny (fallback when no extension gates)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
// env: AGY_APPROVALS=shadow AGY_APPROVALS_MODE=ask
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`auto` (default) keeps the gate OFF until one of the known pi permission packages is detected (pi settings `packages` name-match or known config markers). `shadow` forces it on; `off` forces it off. `dedicated` currently stages the same shadow tools (the explicit `antigravity_approve` variant is planned; the config value is accepted today so the schema is stable). The fallback `mode` is consulted only when no extension blocked the call: `ask` shows a pi confirm dialog (headless runs deny, fail-closed), `allow` approves, `deny` blocks. Timeouts deny fail-closed: the staged hook timeout always exceeds the park budget, but a hook that outlives its timeout soft-passes upstream (verified against the ACP server), so the park always answers first.
|
|
20
|
+
|
|
21
|
+
If you write your own gate extension, it sees a normal pi tool call:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
export default function (pi) {
|
|
25
|
+
pi.on("tool_call", async (event) => {
|
|
26
|
+
if (event.input?.__agyGate && event.input.command?.startsWith("rm ")) {
|
|
27
|
+
return { block: true, reason: "rm is not allowed through the agy gate" };
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Live end-to-end verification (drive a real mutating agy turn through the gate) is still pending; see docs/TODO.md section 1.
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -32,6 +32,7 @@ src/approval-gate.ts shadow tool factory (bash/write/edit): marker calls are ti
|
|
|
32
32
|
src/approval-detect.ts third-party pi permission-extension detection; resolves approvals.gateMode auto (off until a gate extension exists)
|
|
33
33
|
src/approval-hook.ts merge-safe .agents/hooks.json staging (PreToolUse) + generated 0600 early-ack/poll hook script; staged timeout exceeds the park budget (hook timeouts soft-pass)
|
|
34
34
|
src/diff-render.ts stream-json: render agy's file edits as git diffs in pi's thinking stream; formatInlineDiff (no git) renders ACP's native diffs
|
|
35
|
+
src/engine-picker.ts first-run onboarding: engine picker overlay (SelectList + DynamicBorder), first-run gate (no config file + no AGY_ENGINE), agy binary detection (PATH / AGY_BIN), missing-CLI toast copy
|
|
35
36
|
src/driver-types.ts TurnDriver contract shared by both engines (request/handle/snapshot types)
|
|
36
37
|
src/acp/jsonrpc.ts NDJSON JSON-RPC 2.0 framing with line buffering and typed error results
|
|
37
38
|
src/acp/connection.ts ACP server process + protocol (initialize, session/new+load, prompt with image/resource blocks, config options, cancel probing, auto permissions)
|
package/docs/DEVELOPMENT.md
CHANGED
|
@@ -99,6 +99,7 @@ Most "stuck" reports trace to one of:
|
|
|
99
99
|
- `tests/acp-config.test.ts` - engine selection narrowing (`AGY_ENGINE`/`config.engine`), acp block parsing.
|
|
100
100
|
- `tests/daily-log.test.ts` - the support log: day rotation, retention cutoff boundary, secret redaction (incl. header blocks), the 4 KB record cap, never-throw on a broken dir, and the two-tier gate (debug records dropped unless `AGY_DEBUG`).
|
|
101
101
|
- `tests/mcp-registration.test.ts` - the `~/.gemini/config/mcp_config.json` registration for the stream-json CLI: exact agy entry shape, foreign servers preserved, corrupt config refused, atomic writes, stale-entry sweep.
|
|
102
|
+
- `tests/engine-picker.test.ts` - first-run onboarding: picker gate (no config file + no AGY_ENGINE, fail-closed fs), option order (stream-json first), the ACP download/sign-in disclosure pins (intro + saved toast), agy binary detection (PATH scan, explicit path, fail-closed), missing-CLI toast copy.
|
|
102
103
|
- `tests/provider-escalation.test.ts` - the early-ack + poll pipeline (escalation registry, poll views, late-delivery tombstones) and tool-result image forwarding on both engines.
|
|
103
104
|
- `tests/approval-gate.test.ts` - the shadow tool factory: marker calls never execute, ticket verification denies forged/stale markers before the policy, denials throw, native-to-shadow mapping.
|
|
104
105
|
- `tests/approval-park.test.ts` - the approval park end-to-end over a real (port 0) server: POST ticket early-ack, poll pending -> terminal, timeout deny, ungated/unwired deny, close fail-closed, provider round-trip (allow / block-deny / timeout), `__agy*` strip on G9 args.
|
package/docs/ENGINES.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Engines
|
|
2
|
+
|
|
3
|
+
Turns run through one of two engines behind the same provider surface (`config.engine`, default `stream-json`). The choice of engine is left to the user: a first-run picker explains the trade-offs and asks once, and `/agy engine` (no arguments, TUI) reopens the same modal anytime. Direct switches work too: `/agy engine acp|stream-json`.
|
|
4
|
+
|
|
5
|
+
- **stream-json** (default): the persistent `agy` CLI process. The tested default; live token usage; conversation resume via `--conversation`.
|
|
6
|
+
- **acp** (beta): Google's official ACP server (`agy_acp_server.par`), JSON-RPC 2.0 over stdio. Beta: parity-verified live against the current build (RC01) - text streaming, multi-turn resume via `session/load`, bridge tools, effort switching, serialization, abort recovery (see `scripts/parity-live.mjs`). Two known RC01 gaps remain: no usage fields (token display shows client-side ESTIMATES until Google ships usage; `acp.usageEstimate` off to keep zeros) and no cancel (abort tears the server down and reloads it next turn).
|
|
7
|
+
|
|
8
|
+
## First run
|
|
9
|
+
|
|
10
|
+
On a fresh install (no `config.json` yet), the first interactive pi start opens a picker modal that explains both engines - stream-json needs the `agy` CLI installed and authenticated; ACP needs a second Google sign-in plus a ~1.5 GB server binary downloaded from Google. stream-json is preselected (the default); `esc` decides later (nothing is written, the modal reappears on the next start).
|
|
11
|
+
|
|
12
|
+
Picking **acp** starts the server download immediately (progress in the status bar, milestones in the chat), then opens the Google sign-in; a restart applies the engine. Picking **stream-json** persists and toasts; while that engine is active and the `agy` binary is missing, pi warns on every start with the install link until the binary shows up.
|
|
13
|
+
|
|
14
|
+
## Capabilities
|
|
15
|
+
|
|
16
|
+
| Capability | `stream-json` (default) | `acp` (beta) |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| Show thinking text | No (token count only, floor 64, no text body) | Yes (streams thought text via `agent_thought_chunk`; sparse on RC01 where reasoning often arrives in message text) |
|
|
19
|
+
| Live token usage | Yes (live metrics from CLI step events) | Estimated client-side (absent in RC01; `acp.usageEstimate`, default on) |
|
|
20
|
+
| Image prompt input | No (CLI prompt is text-only; images dropped) | Yes (native image blocks forwarded to server) |
|
|
21
|
+
| Image tool results | Yes (bridge tool results carry pixels; probe-verified 2026-09-07) | Yes (probe-verified 2026-09-05) |
|
|
22
|
+
| Audio prompt input | No (dropped) | Protocol advertised (`promptCapabilities.audio: true`) |
|
|
23
|
+
| Review-only plan mode | Yes (`--mode plan` review-only via `/agy mode plan`) | No (RC01 modes are permission levels; plan mode refused) |
|
|
24
|
+
| Leading slash commands in prompt | Disabled via `--disable-slash-commands` (sent as plain text) | Server intercepts recognized commands (e.g. `/plan`) and executes them under the active policy |
|
|
25
|
+
| Dynamic model / effort switch | Recycles process on model or effort change | Dynamic per-turn via `session/set_config_option` (no restart) |
|
|
26
|
+
| Process lifecycle | 1 persistent `agy` process per provider; recycles on drift | 1 persistent server process hosting N sessions concurrently |
|
|
27
|
+
| Session resume & persistence | Client-side map in `sessions.json` via `--conversation <id>` | Server-side session store via `session/load` and `session/new` |
|
|
28
|
+
| Turn cancel / abort | Kills process group; in-flight turn terminates | Teardown, kill, and auto-reload on RC01 (-32601 fallback) |
|
|
29
|
+
| MCP tool bridge routing | Injected filesystem config via `--add-dir` | Direct `mcpServers` param in `session/new` and `session/load` |
|
|
30
|
+
| Tool execution & visibility | Native re-exec (read-only) + wrapper replay (mutating) | Server executes tools natively; events stream with content |
|
|
31
|
+
| Inline file edit diffs | Sourced from git working tree in thinking block | Sourced from `tool_call content[]` or disk vs git HEAD |
|
|
32
|
+
| Permission handling | `--dangerously-skip-permissions` (unattended CLI requirement) | Protocol-native `session/request_permission` (auto-approve when `skipPermissions` is on; auto-deny when off) |
|
|
33
|
+
| Context digest delivery (G1) | Prepend plain text inline in prompt | Native `embeddedContext` resource block |
|
|
34
|
+
| System prompt delivery (G10) | Prepend to first prompt of conversation | Prepend to first prompt of conversation |
|
|
35
|
+
| Authentication methods | Inherits existing `agy` CLI OAuth state | 4 methods: `oauth-personal`, `oauth-business`, `gemini-api-key`, `agent-platform` |
|
|
36
|
+
| Wire protocol | Undocumented CLI NDJSON stream format | Versioned JSON-RPC 2.0 over stdio (`protocolVersion: 1`) |
|
|
37
|
+
| Diagnostics (`/agy doctor`) | Child PID, state, process spawns, recycles, queue stats | Server version, agentInfo, session counts, reconnect count, cancel support |
|
|
38
|
+
| Integration channel | Spawns internal CLI stream-json dialect | Official Google first-party ACP server binary |
|
|
39
|
+
|
|
40
|
+
## Engine-dependent features
|
|
41
|
+
|
|
42
|
+
pi image attachments ride natively only on the ACP engine (the picker offers image attach automatically when `config.engine` is `acp`; the stream-json CLI prompt is text-only). With the optional G1 digest enabled, its delivery also differs: ACP ships it as a native `embeddedContext` resource block, stream-json prepends it to the prompt text. The `AskAntigravity` delegation tool is unaffected by `config.engine` and runs the `stream-json` CLI (`agy -p`) across both configurations.
|
|
43
|
+
|
|
44
|
+
## Switching and setup
|
|
45
|
+
|
|
46
|
+
Switch with `/agy engine acp|stream-json` (takes effect on restart), or run `/agy engine` with no arguments for the same picker modal as first run (an `acp` pick there runs the same download + sign-in chain). Setup is automatic: switching to `acp` installs Google's official ACP server binary from the [antigravity-acp registry entry](https://github.com/agentclientprotocol/registry) (`~/.local/opt/agy-acp/<build>/` + a `current` symlink, zip sha256 recorded; layout and pinning in [docs/ACP-ADOPTION-PLAN.md](ACP-ADOPTION-PLAN.md)) and prepares the login. The login is your Antigravity subscription: the same account and plan you use for the Antigravity CLI (`agy`). Sign in explicitly with `/agy auth` (engine `acp` selected): it opens the Google login in your browser and completes when you finish it. If no browser is available (an SSH session on a remote machine), pi shows the sign-in URL to copy, plus the ssh port-forward command for the login redirect. It is no different from logging into the CLI; the server just keeps its own token file on your machine, like any Google tool, and this extension never sees your credentials. If you also export `GEMINI_API_KEY`, it is ignored: the server uses the auth type in settings.json, and setup always writes `oauth-personal`. A session start self-heals the same way, silently when everything is ready. Manual instructions (`/agy auth-manual`) surface only when a step fails. Sessions are engine-scoped, so switching engines never crosses conversations.
|
package/docs/TODO.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## 1. Approval gate: live end-to-end verification
|
|
4
4
|
|
|
5
|
-
The gate is wired and unit-pinned (
|
|
5
|
+
The gate is wired and unit-pinned (333 tests) but has never run against a live
|
|
6
6
|
agy turn. Enable it, drive an agy turn that mutates a file, and watch the
|
|
7
7
|
round trip: PreToolUse hook -> POST /approval park -> shadow toolUse ->
|
|
8
8
|
decision -> hook stdout -> agy enforces.
|
package/extensions/index.ts
CHANGED
|
@@ -66,6 +66,7 @@ import { setupAuthUrlCapture } from "../src/acp/browser-capture.js";
|
|
|
66
66
|
import { ensureAcpReady, inspectAcpSetup } from "../src/acp/setup.js";
|
|
67
67
|
import type { TurnDriver, TurnOutcome } from "../src/driver-types.js";
|
|
68
68
|
import { CONFIG_PATH, loadConfig, logsDir, saveConfig, type AgyMode, type BridgeTools, type Engine, type ThinkingTier } from "../src/config.js";
|
|
69
|
+
import { agyMissingMessage, isAgyInstalled, savedEngineMessage, showEnginePicker, shouldOfferEnginePicker } from "../src/engine-picker.js";
|
|
69
70
|
import { createDailyLogger, type DailyLogger } from "../src/daily-log.js";
|
|
70
71
|
import { registerAskAntigravityTool, toolModelsFromRaw } from "../src/ask-tool.js";
|
|
71
72
|
import { startMcpServer, TOKEN_HEADER, type McpServerHandle } from "../src/mcp-server.js";
|
|
@@ -181,6 +182,8 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
181
182
|
// ACP self-heal runs once per process (session_start re-fires on /reload;
|
|
182
183
|
// a ready setup is two file stats, so re-running is harmless anyway).
|
|
183
184
|
let acpSelfHealRan = false;
|
|
185
|
+
// Warn-once-per-process flag for the missing-agy-CLI toast (stream-json).
|
|
186
|
+
let agyMissingWarned = false;
|
|
184
187
|
// OAuth URL capture: the server hands the login URL only to the
|
|
185
188
|
// browser-open call (nothing on stdio), so a BROWSER wrapper records it
|
|
186
189
|
// and the driver logs it as "auth-url". Local users keep the automatic
|
|
@@ -276,6 +279,10 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
276
279
|
// Resolved per connection: the setup flow can install the binary and
|
|
277
280
|
// update acp.bin mid-session; the next turn picks it up (no restart).
|
|
278
281
|
bin: () => loadConfig().acp.bin,
|
|
282
|
+
// Resolved per turn: /agy and AGY_USAGE_ESTIMATE changes apply without
|
|
283
|
+
// a restart. Without this the driver defaults to "estimate" and the
|
|
284
|
+
// config knob (incl. "off") is dead.
|
|
285
|
+
usageEstimate: () => loadConfig().acp.usageEstimate,
|
|
279
286
|
...(authCapture ? { extraEnv: authCapture.browserEnv, authUrlFile: authCapture.file } : {}),
|
|
280
287
|
log: acpLog,
|
|
281
288
|
mcpServers: () => {
|
|
@@ -380,6 +387,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
380
387
|
acpLog,
|
|
381
388
|
fileLog,
|
|
382
389
|
authCapture: authCapture ?? null,
|
|
390
|
+
runAcpPickSetup,
|
|
383
391
|
});
|
|
384
392
|
|
|
385
393
|
// AskAntigravity tool: one-shot delegation to agy (ported from
|
|
@@ -412,12 +420,115 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
412
420
|
},
|
|
413
421
|
});
|
|
414
422
|
|
|
423
|
+
/** ACP pick follow-through (first-run wizard): the same self-service
|
|
424
|
+
* setup the /agy engine acp command runs, but immediately - the 1.5 GB
|
|
425
|
+
* download starts while the toast is still on screen, progress rides the
|
|
426
|
+
* footer status, and the Google sign-in opens when the install lands.
|
|
427
|
+
* Restart still applies the engine (drivers wire at load); this only
|
|
428
|
+
* removes the wait. Fire-and-forget: the caller already toasted the
|
|
429
|
+
* promise, failures land in the daily log + a warning toast. */
|
|
430
|
+
// eslint-disable-next-line @typescript-eslint/no-inner-declarations -- hoisted: registerAgyCommand below injects it
|
|
431
|
+
async function runAcpPickSetup(ctx: { ui: ExtensionUIContext }): Promise<void> {
|
|
432
|
+
acpSelfHealRan = true;
|
|
433
|
+
let lastPhase = "";
|
|
434
|
+
ctx.ui.setStatus("agy-acp", "downloading ACP server…");
|
|
435
|
+
try {
|
|
436
|
+
const status = await ensureAcpReady({
|
|
437
|
+
configBin: loadConfig().acp.bin,
|
|
438
|
+
onProgress: (m) => {
|
|
439
|
+
// Dual surface: the status bar carries the live percent (cleared
|
|
440
|
+
// on completion, zero footprint); the chat window gets phase
|
|
441
|
+
// milestones only (download start, unpacking, installed) - same
|
|
442
|
+
// line-in-chat feel as other extensions' notify() notices. The
|
|
443
|
+
// percent variant updates every chunk and would spam the chat.
|
|
444
|
+
ctx.ui.setStatus("agy-acp", m);
|
|
445
|
+
if (m !== lastPhase && !/\d+%/.test(m)) {
|
|
446
|
+
ctx.ui.notify(m, "info");
|
|
447
|
+
lastPhase = m;
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
ctx.ui.setStatus("agy-acp", undefined);
|
|
452
|
+
fileLog.log(
|
|
453
|
+
"acp-setup",
|
|
454
|
+
status.ok
|
|
455
|
+
? { ok: true, binarySource: status.binarySource, needsLogin: status.needsLogin }
|
|
456
|
+
: { ok: false, error: status.error },
|
|
457
|
+
status.ok ? "info" : "warn",
|
|
458
|
+
);
|
|
459
|
+
if (!status.ok) {
|
|
460
|
+
ctx.ui.notify(`ACP auto-setup failed (${status.error}).\n${status.manual}`, "warning");
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
// Spread, not a bare acp patch: a bare {bin} patch would drop
|
|
464
|
+
// sibling keys (usageEstimate) from the file.
|
|
465
|
+
saveConfig({ acp: { ...loadConfig().acp, bin: status.bin } });
|
|
466
|
+
if (!status.needsLogin) {
|
|
467
|
+
ctx.ui.notify(`ACP server ready (auth: ${status.auth}). Restart applies the engine.`, "info");
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
ctx.ui.notify(
|
|
471
|
+
"ACP server ready. Signing in: the Google sign-in opens in your browser and completes when you finish it.",
|
|
472
|
+
"info",
|
|
473
|
+
);
|
|
474
|
+
const r = await runAcpAuth({
|
|
475
|
+
bin: status.bin,
|
|
476
|
+
...(authCapture ? { extraEnv: authCapture.browserEnv, authUrlFile: authCapture.file } : {}),
|
|
477
|
+
log: acpLog,
|
|
478
|
+
});
|
|
479
|
+
fileLog.log("acp-auth", r.ok ? { ok: true } : { ok: false, error: r.error }, r.ok ? "info" : "warn");
|
|
480
|
+
if (r.ok) ctx.ui.notify("Signed in. The ACP engine is ready; restart applies it.", "info");
|
|
481
|
+
else ctx.ui.notify(`ACP sign-in failed (${r.error}).\nRun /agy auth to retry; /agy auth-manual has manual steps.`, "warning");
|
|
482
|
+
} catch (err) {
|
|
483
|
+
ctx.ui.setStatus("agy-acp", undefined);
|
|
484
|
+
fileLog.log("acp-setup", { error: String(err) }, "warn");
|
|
485
|
+
ctx.ui.notify(`ACP setup failed (${String(err)}). /agy auth retries; /agy doctor inspects.`, "warning");
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
415
489
|
// MCP tool bridge: expose pi's tools to agy over localhost Streamable HTTP.
|
|
416
490
|
// Calls park in the provider's round-trip store and complete through pi's
|
|
417
491
|
// normal toolUse loop (native cards, permissions, hooks) - no patch, no
|
|
418
492
|
// privileged API. Started on session_start, torn down on session_shutdown.
|
|
419
|
-
pi.on("session_start", async (
|
|
493
|
+
pi.on("session_start", async (event, ctx) => {
|
|
420
494
|
if (ctx.hasUI) activeUi = ctx.ui;
|
|
495
|
+
// First-run engine picker: ask once, on the first interactive start,
|
|
496
|
+
// which turn engine to use. Skipped headless (ctx.mode !== "tui"),
|
|
497
|
+
// when AGY_ENGINE is set, or once any config file exists (any save -
|
|
498
|
+
// even of an unrelated knob - means the user has been here before).
|
|
499
|
+
// esc = decide later: nothing is written, the picker reappears next
|
|
500
|
+
// start. Like /agy engine, the choice applies on the next start
|
|
501
|
+
// (drivers wire at load). The await intentionally runs before the
|
|
502
|
+
// bridge startup below: on a genuine first run the modal blocks input
|
|
503
|
+
// anyway, so the delay is invisible.
|
|
504
|
+
if (event.reason === "startup" && ctx.mode === "tui" && shouldOfferEnginePicker(CONFIG_PATH)) {
|
|
505
|
+
// Best-effort, like the legacy-patch notice below: a picker failure
|
|
506
|
+
// (mid-prompt TUI teardown, resize races) must never take down the
|
|
507
|
+
// rest of session_start - the MCP bridge startup included. The
|
|
508
|
+
// default engine keeps working untouched.
|
|
509
|
+
try {
|
|
510
|
+
const picked = await showEnginePicker(ctx.ui);
|
|
511
|
+
if (picked) {
|
|
512
|
+
saveConfig({ engine: picked });
|
|
513
|
+
ctx.ui.notify(savedEngineMessage(picked), "info");
|
|
514
|
+
if (picked === "acp") void runAcpPickSetup(ctx);
|
|
515
|
+
}
|
|
516
|
+
} catch (err) {
|
|
517
|
+
fileLog.log("engine-picker", { error: String(err) }, "warn");
|
|
518
|
+
console.error(`[antigravity-bridge] engine picker failed: ${String(err)}`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// agy presence check (stream-json engine): the CLI is the whole engine,
|
|
522
|
+
// so a missing binary means every Antigravity turn would fail. Warn on
|
|
523
|
+
// every process start until it is installed (per-process flag so /new,
|
|
524
|
+
// /resume and /reload re-fires do not nag mid-session). Runs after the
|
|
525
|
+
// picker above, so a first-run stream-json pick warns immediately.
|
|
526
|
+
if (engine === "stream-json" && !agyMissingWarned && !isAgyInstalled(binary)) {
|
|
527
|
+
agyMissingWarned = true;
|
|
528
|
+
const msg = agyMissingMessage();
|
|
529
|
+
if (ctx.hasUI) ctx.ui.notify(msg, "warning");
|
|
530
|
+
else console.error(`[antigravity-bridge] ${msg}`);
|
|
531
|
+
}
|
|
421
532
|
// Legacy cleanup: users who ran the old consent-gated patcher still
|
|
422
533
|
// carry pi.invokeTool in their installed pi. Inert, but tell them once
|
|
423
534
|
// and offer /agy patch-cleanup. Never auto-edits the install.
|
|
@@ -451,7 +562,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
451
562
|
);
|
|
452
563
|
if (status.ok) {
|
|
453
564
|
if (status.binarySource === "installed" || status.binarySource === "existing") {
|
|
454
|
-
saveConfig({ acp: { bin: status.bin
|
|
565
|
+
saveConfig({ acp: { ...loadConfig().acp, bin: status.bin } });
|
|
455
566
|
}
|
|
456
567
|
if (status.needsLogin) {
|
|
457
568
|
const msg = acpLoginPending();
|
|
@@ -755,6 +866,9 @@ interface AgyCommandCtx {
|
|
|
755
866
|
acpLog: (msg: string, data?: unknown) => void;
|
|
756
867
|
/** Daily file logger (src/daily-log.ts); command + doctor surfacing. */
|
|
757
868
|
fileLog: DailyLogger;
|
|
869
|
+
/** Wizard-pick follow-through (download now + chained sign-in); reused
|
|
870
|
+
* by /agy engine's no-args modal so both entry points behave alike. */
|
|
871
|
+
runAcpPickSetup: (cmdCtx: { ui: ExtensionUIContext }) => Promise<void>;
|
|
758
872
|
/** BROWSER-capture handles; null when unavailable (Windows, unwritable
|
|
759
873
|
* data dir). /agy auth passes them to the sign-in process. */
|
|
760
874
|
authCapture: { browserEnv: Record<string, string>; file: string } | null;
|
|
@@ -866,7 +980,7 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
866
980
|
ui?.notify(`ACP auto-setup failed (${status.error}).\n${status.manual}`, "warning");
|
|
867
981
|
return;
|
|
868
982
|
}
|
|
869
|
-
saveConfig({ acp: { bin: status.bin
|
|
983
|
+
saveConfig({ acp: { ...loadConfig().acp, bin: status.bin } });
|
|
870
984
|
if (status.needsLogin) {
|
|
871
985
|
ui?.notify(
|
|
872
986
|
`ACP engine set. ${acpLoginPending()}`,
|
|
@@ -875,6 +989,28 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
875
989
|
} else {
|
|
876
990
|
ui?.notify(`ACP engine ready (auth: ${status.auth}). Takes effect on the next pi start (or /reload).`, "info");
|
|
877
991
|
}
|
|
992
|
+
} else if (!val && mode === "tui" && ui) {
|
|
993
|
+
// Same modal as the first-run wizard: switching engines deserves
|
|
994
|
+
// the explanations, not a bare usage line. Semantics match the
|
|
995
|
+
// direct path above: plan blocks acp, an acp pick chains setup
|
|
996
|
+
// + sign-in immediately, restart applies the switch.
|
|
997
|
+
const current = loadConfig().engine;
|
|
998
|
+
const picked = await showEnginePicker(ui);
|
|
999
|
+
if (picked === null) {
|
|
1000
|
+
ui.notify(`engine unchanged: ${current}.`, "info");
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
if (picked === current) {
|
|
1004
|
+
ui.notify(`engine is already ${current}. Restart applies it if set this session.`, "info");
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
if (picked === "acp" && loadConfig().mode === "plan") {
|
|
1008
|
+
ui.notify("mode is plan; the ACP engine has no plan mode. /agy mode accept-edits first.", "warning");
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
saveConfig({ engine: picked });
|
|
1012
|
+
ui.notify(savedEngineMessage(picked), "info");
|
|
1013
|
+
if (picked === "acp") void ctx.runAcpPickSetup({ ui });
|
|
878
1014
|
} else {
|
|
879
1015
|
ui?.notify(`current engine: ${loadConfig().engine}\nusage: /agy engine stream-json|acp`, "info");
|
|
880
1016
|
}
|
|
@@ -901,7 +1037,7 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
901
1037
|
ui?.notify(`ACP auto-setup failed (${status.error}).\n${status.manual}`, "warning");
|
|
902
1038
|
return;
|
|
903
1039
|
}
|
|
904
|
-
saveConfig({ acp: { bin: status.bin
|
|
1040
|
+
saveConfig({ acp: { ...loadConfig().acp, bin: status.bin } });
|
|
905
1041
|
if (!status.needsLogin) {
|
|
906
1042
|
ui?.notify(`Already signed in (auth: ${status.auth}). Nothing to do.`, "info");
|
|
907
1043
|
return;
|
|
@@ -929,7 +1065,9 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
929
1065
|
if (rest.length > 0) {
|
|
930
1066
|
// Only the keyword compares case-insensitively; the path keeps its case.
|
|
931
1067
|
const bin = rest.toLowerCase() === "auto" ? "" : rest.replace(/^~(?=\/|$)/, os.homedir());
|
|
932
|
-
|
|
1068
|
+
// Spread, not a bare acp patch: a bare {bin, permissions} object
|
|
1069
|
+
// would drop sibling keys (usageEstimate) from the file.
|
|
1070
|
+
saveConfig({ acp: { ...loadConfig().acp, bin } });
|
|
933
1071
|
ui?.notify(
|
|
934
1072
|
bin
|
|
935
1073
|
? `acp.bin set to ${bin}. The next ACP turn (re)connects with it.`
|
|
@@ -999,6 +1137,8 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
999
1137
|
// line the day it starts (then real usage mapping is worth wiring).
|
|
1000
1138
|
if (snap.acp.usageSeen) {
|
|
1001
1139
|
lines.push(" acp tokens: AVAILABLE in server payloads (wire real usage mapping next)");
|
|
1140
|
+
} else if (config.acp.usageEstimate !== "off") {
|
|
1141
|
+
lines.push(` acp tokens: ESTIMATED client-side (mode: ${config.acp.usageEstimate}; auto-off once the server sends real usage)`);
|
|
1002
1142
|
}
|
|
1003
1143
|
}
|
|
1004
1144
|
if (snap.lifecycle.length > 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@estebanforge/pi-antigravity-bridge",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Gemini provider for Pi on the Antigravity ACP server (official Google ACP) or the stream-json agy CLI. antigravity/* models in Pi's /model picker, no-patch MCP bridge: agy runs Pi's tools. ToS safe to use.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
package/src/acp/driver.ts
CHANGED
|
@@ -19,9 +19,12 @@
|
|
|
19
19
|
// in-connection (plan §9.3); no provider involvement
|
|
20
20
|
|
|
21
21
|
import { randomUUID } from "node:crypto";
|
|
22
|
+
import type { UsageEstimate } from "../config.js";
|
|
22
23
|
import { AcpConnection, resolveAcpBinary, type AcpMcpServer } from "./connection.js";
|
|
23
24
|
import { mapStopReason, mapUpdate, TextAccumulator, type AcpEditDiff } from "./events.js";
|
|
25
|
+
import { estimateTokens, synthesizeUsage } from "./usage-estimate.js";
|
|
24
26
|
import type {
|
|
27
|
+
AgyUsage,
|
|
25
28
|
DriverActivity,
|
|
26
29
|
DriverSnapshot,
|
|
27
30
|
DriverState,
|
|
@@ -46,6 +49,10 @@ export interface AcpDriverOptions {
|
|
|
46
49
|
authUrlFile?: string;
|
|
47
50
|
/** Bridge registration for session/new AND session/load. */
|
|
48
51
|
mcpServers?: () => AcpMcpServer[];
|
|
52
|
+
/** Gate B stopgap: how ACP turns synthesize usage while the server sends
|
|
53
|
+
* none. A function is resolved per turn (follows live config, like bin).
|
|
54
|
+
* Default "estimate". */
|
|
55
|
+
usageEstimate?: UsageEstimate | (() => UsageEstimate);
|
|
49
56
|
log?: (msg: string, data?: unknown) => void;
|
|
50
57
|
}
|
|
51
58
|
|
|
@@ -59,6 +66,13 @@ interface ActiveTurn {
|
|
|
59
66
|
resolve: (o: TurnOutcome) => void;
|
|
60
67
|
outcome: Promise<TurnOutcome>;
|
|
61
68
|
response: TextAccumulator;
|
|
69
|
+
/** Usage-synthesis counters: per-delta token sums (estimate mode) and
|
|
70
|
+
* delta counts (direct mode). Sums are taken per delta, never over the
|
|
71
|
+
* joined text, so a word split across chunks stays two words. */
|
|
72
|
+
textTokens: number;
|
|
73
|
+
thoughtTokens: number;
|
|
74
|
+
textDeltas: number;
|
|
75
|
+
thoughtDeltas: number;
|
|
62
76
|
sawResult: boolean;
|
|
63
77
|
/** True once the prompt RPC was issued. Abort before this point has
|
|
64
78
|
* nothing to cancel: probing would risk a success-as-noop answer from a
|
|
@@ -359,10 +373,16 @@ export class AcpDriver implements TurnDriver {
|
|
|
359
373
|
switch (mapped.kind) {
|
|
360
374
|
case "text": {
|
|
361
375
|
const emit = turn.response.append(mapped.delta);
|
|
362
|
-
if (emit)
|
|
376
|
+
if (emit) {
|
|
377
|
+
turn.textDeltas += 1;
|
|
378
|
+
turn.textTokens += estimateTokens(emit);
|
|
379
|
+
this.#emit(turn, { type: "text", delta: emit });
|
|
380
|
+
}
|
|
363
381
|
return;
|
|
364
382
|
}
|
|
365
383
|
case "thought": {
|
|
384
|
+
turn.thoughtDeltas += 1;
|
|
385
|
+
turn.thoughtTokens += estimateTokens(mapped.delta);
|
|
366
386
|
this.#emit(turn, { type: "thought", delta: mapped.delta });
|
|
367
387
|
return;
|
|
368
388
|
}
|
|
@@ -543,6 +563,10 @@ export class AcpDriver implements TurnDriver {
|
|
|
543
563
|
resolve,
|
|
544
564
|
outcome,
|
|
545
565
|
response: new TextAccumulator(),
|
|
566
|
+
textTokens: 0,
|
|
567
|
+
thoughtTokens: 0,
|
|
568
|
+
textDeltas: 0,
|
|
569
|
+
thoughtDeltas: 0,
|
|
546
570
|
sawResult: false,
|
|
547
571
|
promptStarted: false,
|
|
548
572
|
aborted: false,
|
|
@@ -643,6 +667,16 @@ export class AcpDriver implements TurnDriver {
|
|
|
643
667
|
|
|
644
668
|
#settle(turn: ActiveTurn, outcome: TurnOutcome): void {
|
|
645
669
|
if (turn.closed) return;
|
|
670
|
+
// Gate B stopgap: synthesize usage on clean turns while the server sends
|
|
671
|
+
// none. Runs BEFORE close so the usage activity drains through the
|
|
672
|
+
// normal #nextActivity loop (provider maps it onto partial.usage).
|
|
673
|
+
if (outcome.status === "OK" && !outcome.aborted && outcome.usage === undefined) {
|
|
674
|
+
const usage = this.#syntheticUsage(turn);
|
|
675
|
+
if (usage) {
|
|
676
|
+
outcome.usage = usage;
|
|
677
|
+
this.#emit(turn, { type: "usage", usage });
|
|
678
|
+
}
|
|
679
|
+
}
|
|
646
680
|
turn.closed = true;
|
|
647
681
|
if (turn.overallTimer) clearTimeout(turn.overallTimer);
|
|
648
682
|
if (turn.idleTimer) clearTimeout(turn.idleTimer);
|
|
@@ -671,6 +705,29 @@ export class AcpDriver implements TurnDriver {
|
|
|
671
705
|
});
|
|
672
706
|
}
|
|
673
707
|
|
|
708
|
+
/** Usage synthesis mode, resolved per turn (follows live config). */
|
|
709
|
+
#usageMode(): UsageEstimate {
|
|
710
|
+
const opt = this.#opts.usageEstimate;
|
|
711
|
+
return (typeof opt === "function" ? opt() : opt) ?? "estimate";
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
#syntheticUsage(turn: ActiveTurn): AgyUsage | undefined {
|
|
715
|
+
const mode = this.#usageMode();
|
|
716
|
+
if (mode === "off") return undefined;
|
|
717
|
+
// Gate B latch: any server frame with usage/token keys means real
|
|
718
|
+
// usage exists upstream; estimates must never shadow it.
|
|
719
|
+
if (this.#conn?.usageSeen) return undefined;
|
|
720
|
+
return synthesizeUsage({
|
|
721
|
+
mode,
|
|
722
|
+
prompt: turn.request.prompt,
|
|
723
|
+
contextText: turn.request.contextBlock?.text,
|
|
724
|
+
textTokens: turn.textTokens,
|
|
725
|
+
thoughtTokens: turn.thoughtTokens,
|
|
726
|
+
textDeltas: turn.textDeltas,
|
|
727
|
+
thoughtDeltas: turn.thoughtDeltas,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
|
|
674
731
|
#log(msg: string, data?: unknown): void {
|
|
675
732
|
const line = `${new Date().toISOString().slice(11, 19)} ${msg}${data !== undefined ? ` ${JSON.stringify(data)}` : ""}`;
|
|
676
733
|
this.#lifecycle.push(line);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Gate B stopgap: synthesize AgyUsage for ACP turns until Google ships a
|
|
2
|
+
// real token-usage layer in agy_acp_server. Counting mechanism copied from
|
|
3
|
+
// the pi-token-speed extension (npm:pi-token-speed, src/engine.ts): a
|
|
4
|
+
// word/punctuation regex, 1 match = 1 token. The numbers are client-side
|
|
5
|
+
// ESTIMATES, never provider data: the connection's usageSeen latch (Gate B
|
|
6
|
+
// watch) keeps synthesis off the day real usage appears in any frame, so
|
|
7
|
+
// estimates can never shadow real numbers.
|
|
8
|
+
|
|
9
|
+
import type { UsageEstimate } from "../config.js";
|
|
10
|
+
import type { AgyUsage } from "../driver-types.js";
|
|
11
|
+
|
|
12
|
+
const TOKEN_REGEX = /\w+|[^\s\w]/g;
|
|
13
|
+
|
|
14
|
+
/** Word-boundary token estimate (pi-token-speed's estimateTokens). */
|
|
15
|
+
export function estimateTokens(text: string): number {
|
|
16
|
+
if (!text) return 0;
|
|
17
|
+
const matches = text.match(TOKEN_REGEX);
|
|
18
|
+
return matches ? matches.length : 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface UsageEstimateInput {
|
|
22
|
+
mode: UsageEstimate;
|
|
23
|
+
/** Full outgoing prompt text (input side; always regex-estimated). */
|
|
24
|
+
prompt: string;
|
|
25
|
+
/** ACP embeddedContext resource text (G1 digest): also reaches the model. */
|
|
26
|
+
contextText?: string;
|
|
27
|
+
/** Per-delta token sums (mode "estimate"). Summing per delta, like
|
|
28
|
+
* pi-token-speed's recordDelta, avoids word-merge artifacts when a
|
|
29
|
+
* chunk boundary splits a word. */
|
|
30
|
+
textTokens: number;
|
|
31
|
+
thoughtTokens: number;
|
|
32
|
+
/** Streamed delta counts (mode "direct": 1 token per delta). */
|
|
33
|
+
textDeltas: number;
|
|
34
|
+
thoughtDeltas: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Synthesize usage for a finished turn, or undefined when nothing is worth
|
|
38
|
+
* reporting (mode off, or a turn with no prompt and no output). */
|
|
39
|
+
export function synthesizeUsage(input: UsageEstimateInput): AgyUsage | undefined {
|
|
40
|
+
if (input.mode === "off") return undefined;
|
|
41
|
+
const inputTokens = estimateTokens(
|
|
42
|
+
input.contextText ? `${input.prompt}\n${input.contextText}` : input.prompt,
|
|
43
|
+
);
|
|
44
|
+
const thoughtTokens =
|
|
45
|
+
input.mode === "direct" ? input.thoughtDeltas : input.thoughtTokens;
|
|
46
|
+
const textTokens =
|
|
47
|
+
input.mode === "direct" ? input.textDeltas : input.textTokens;
|
|
48
|
+
// Thinking folds INTO output (OpenAI convention: reasoning tokens bill as
|
|
49
|
+
// output). toPiUsage drops thinking_tokens, and thinking time is inside
|
|
50
|
+
// elapsed wall time, so folding keeps the tokens/time ratio honest.
|
|
51
|
+
const outputTokens = textTokens + thoughtTokens;
|
|
52
|
+
if (inputTokens === 0 && outputTokens === 0) return undefined;
|
|
53
|
+
return {
|
|
54
|
+
input_tokens: inputTokens,
|
|
55
|
+
output_tokens: outputTokens,
|
|
56
|
+
thinking_tokens: thoughtTokens,
|
|
57
|
+
total_tokens: inputTokens + outputTokens,
|
|
58
|
+
};
|
|
59
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -44,6 +44,12 @@ export type AgyMode = "accept-edits" | "plan";
|
|
|
44
44
|
export type ThinkingTier = "low" | "medium" | "high";
|
|
45
45
|
export type BridgeTools = "none" | "mcp" | "all";
|
|
46
46
|
|
|
47
|
+
/** How ACP turns report token usage while Gate B stands (agy sends none).
|
|
48
|
+
* "estimate" (default): word-boundary regex over prompt/response/thought
|
|
49
|
+
* text (pi-token-speed's mechanism). "direct": 1 token per streamed delta.
|
|
50
|
+
* "off": keep zero-usage. Real server usage (usageSeen latch) always wins. */
|
|
51
|
+
export type UsageEstimate = "estimate" | "direct" | "off";
|
|
52
|
+
|
|
47
53
|
/** How the approval gate activates (docs/TODO.md section 2.5).
|
|
48
54
|
*
|
|
49
55
|
* "auto" (default): OFF until a third-party pi permission extension is
|
|
@@ -74,6 +80,10 @@ export interface AcpConfig {
|
|
|
74
80
|
* (parity with skipPermissions). Kept as a key so future policies do not
|
|
75
81
|
* change the config shape. */
|
|
76
82
|
permissions: "auto";
|
|
83
|
+
/** Gate B stopgap: client-side token estimates for ACP turns so pi's
|
|
84
|
+
* usage surfaces show nonzero numbers. Estimates are labeled as such in
|
|
85
|
+
* /agy doctor and auto-disable when the server sends real usage. */
|
|
86
|
+
usageEstimate: UsageEstimate;
|
|
77
87
|
}
|
|
78
88
|
|
|
79
89
|
export interface AgyConfig {
|
|
@@ -149,7 +159,7 @@ const DEFAULTS: AgyConfig = {
|
|
|
149
159
|
digest: false,
|
|
150
160
|
systemPrompt: true,
|
|
151
161
|
approvals: { gateMode: "auto", mode: "ask" },
|
|
152
|
-
acp: { bin: "", permissions: "auto" },
|
|
162
|
+
acp: { bin: "", permissions: "auto", usageEstimate: "estimate" },
|
|
153
163
|
};
|
|
154
164
|
|
|
155
165
|
/** Load config merged over defaults. Env vars override the file when set. */
|
|
@@ -232,11 +242,19 @@ export function loadConfig(configPath: string = CONFIG_PATH): AgyConfig {
|
|
|
232
242
|
: "ask";
|
|
233
243
|
|
|
234
244
|
const fileAcp = (typeof file.acp === "object" && file.acp !== null ? file.acp : {}) as Partial<AcpConfig>;
|
|
245
|
+
// Unknown values fall back to "estimate" (same narrow-parse pattern as
|
|
246
|
+
// gateMode: a typo must never silently change behavior).
|
|
247
|
+
const usageRaw = String(
|
|
248
|
+
process.env.AGY_USAGE_ESTIMATE ?? fileAcp.usageEstimate ?? DEFAULTS.acp.usageEstimate,
|
|
249
|
+
).toLowerCase();
|
|
250
|
+
const usageEstimate: UsageEstimate =
|
|
251
|
+
usageRaw === "direct" || usageRaw === "off" ? usageRaw : "estimate";
|
|
235
252
|
const acp: AcpConfig = {
|
|
236
253
|
bin:
|
|
237
254
|
process.env.AGY_ACP_BIN ??
|
|
238
255
|
(typeof fileAcp.bin === "string" ? fileAcp.bin : DEFAULTS.acp.bin),
|
|
239
256
|
permissions: "auto",
|
|
257
|
+
usageEstimate,
|
|
240
258
|
};
|
|
241
259
|
|
|
242
260
|
return {
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// First-run onboarding: the engine picker plus the agy-presence warning.
|
|
2
|
+
//
|
|
3
|
+
// On the first interactive start (no config file yet, no AGY_ENGINE env) pi
|
|
4
|
+
// asks which turn engine to use: the stream-json `agy` CLI or Google's
|
|
5
|
+
// official ACP server. The choice persists via saveConfig({ engine }) and,
|
|
6
|
+
// like /agy engine, takes effect on the next pi start (drivers wire at load).
|
|
7
|
+
// Every start with the stream-json engine active also re-checks that the
|
|
8
|
+
// `agy` binary exists and warns until it does (re-auth is out of scope).
|
|
9
|
+
//
|
|
10
|
+
// UI: tui.md "Pattern 1" - SelectList framed by DynamicBorder inside pi's
|
|
11
|
+
// native overlay (the window feel pi-rtk builds its modal on).
|
|
12
|
+
|
|
13
|
+
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import {
|
|
16
|
+
Container,
|
|
17
|
+
type SelectItem,
|
|
18
|
+
SelectList,
|
|
19
|
+
Spacer,
|
|
20
|
+
Text,
|
|
21
|
+
} from "@earendil-works/pi-tui";
|
|
22
|
+
import fs from "node:fs";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
import type { Engine } from "./config.js";
|
|
25
|
+
|
|
26
|
+
/** Picker order is the default answer order: stream-json first. */
|
|
27
|
+
export const ENGINE_PICKER_ITEMS: SelectItem[] = [
|
|
28
|
+
{
|
|
29
|
+
value: "stream-json",
|
|
30
|
+
label: "Stream-JSON CLI",
|
|
31
|
+
description: "default, recommended",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
value: "acp",
|
|
35
|
+
label: "ACP server (official Google)",
|
|
36
|
+
description: "second sign-in, ~1.5 GB binary download",
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/** Intro paragraph above the list. Text wraps, so explanations stay here and
|
|
41
|
+
* SelectItem descriptions stay one-liners (SelectList truncates long lines). */
|
|
42
|
+
export const ENGINE_PICKER_INTRO = [
|
|
43
|
+
"Pick the engine that runs your Antigravity turns. Switch anytime with /agy engine (restart applies it).",
|
|
44
|
+
"",
|
|
45
|
+
"stream-json: the `agy` CLI you already installed and authenticated. Persistent process, streamed output. Tested default.",
|
|
46
|
+
"ACP: Google's official server (agy_acp_server.par). Needs a second Google sign-in and a ~1.5 GB server binary downloaded from Google (automatic, one-time, unavoidable: the server is not part of the agy CLI).",
|
|
47
|
+
].join("\n");
|
|
48
|
+
|
|
49
|
+
/** True only for a genuine first interactive run: no saved config yet (any
|
|
50
|
+
* existing file means the user has been here before) and no AGY_ENGINE env
|
|
51
|
+
* (env wins over the file, so the wizard would fight it). Fails closed to
|
|
52
|
+
* false - an fs error must never nag the user with a dialog. */
|
|
53
|
+
export function shouldOfferEnginePicker(
|
|
54
|
+
configPath: string,
|
|
55
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
56
|
+
): boolean {
|
|
57
|
+
if (env.AGY_ENGINE !== undefined) return false;
|
|
58
|
+
try {
|
|
59
|
+
return !fs.existsSync(configPath);
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Narrow a picker value to an Engine. Items are our own constants, but an
|
|
66
|
+
* unknown value must never reach config as a cast string: it falls back to
|
|
67
|
+
* the tested default. */
|
|
68
|
+
export function toEngine(value: string): Engine {
|
|
69
|
+
return value === "acp" ? "acp" : "stream-json";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Toast copy shown after the choice is saved. ACP names the promise that
|
|
73
|
+
* matters: the binary download starts NOW (not on restart), sign-in follows
|
|
74
|
+
* when it lands, restart applies the engine. */
|
|
75
|
+
export function savedEngineMessage(engine: Engine): string {
|
|
76
|
+
return engine === "acp"
|
|
77
|
+
? "Engine saved: acp. The ~1.5 GB server binary downloads now; the Google sign-in opens when it lands. Restart applies the engine."
|
|
78
|
+
: "Engine saved: stream-json. Restart pi to apply.";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** True when the `agy` CLI binary can be found. A binRef with a path
|
|
82
|
+
* separator (AGY_BIN=/opt/agy/agy) must exist as a file; a bare name is
|
|
83
|
+
* searched on PATH. statSync cannot throw through the guards, but a race
|
|
84
|
+
* (file removed between listing and stat) fails closed to false. */
|
|
85
|
+
export function isAgyInstalled(binRef: string, env: NodeJS.ProcessEnv = process.env): boolean {
|
|
86
|
+
if (binRef.includes("/")) {
|
|
87
|
+
try {
|
|
88
|
+
return fs.statSync(binRef).isFile();
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return (env.PATH ?? "")
|
|
94
|
+
.split(path.delimiter)
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
.some((dir) => {
|
|
97
|
+
try {
|
|
98
|
+
return fs.statSync(path.join(dir, binRef)).isFile();
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Toast copy for the missing-CLI warning. Fires every pi start while the
|
|
106
|
+
* stream-json engine is active and the binary is absent. */
|
|
107
|
+
export function agyMissingMessage(): string {
|
|
108
|
+
return "The `agy` CLI is not installed. Install it from https://antigravity.google/product/antigravity-cli and log in on it to use Antigravity models.";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Render the picker overlay. Resolves with the chosen engine, or null when
|
|
112
|
+
* the user pressed esc (decide later - nothing is persisted). */
|
|
113
|
+
export async function showEnginePicker(ctx: ExtensionUIContext): Promise<Engine | null> {
|
|
114
|
+
return ctx.custom<Engine | null>(
|
|
115
|
+
(tui, theme, _keybindings, done) => {
|
|
116
|
+
const container = new Container();
|
|
117
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
118
|
+
container.addChild(
|
|
119
|
+
new Text(theme.fg("accent", theme.bold("Antigravity Bridge: choose your engine")), 1, 0),
|
|
120
|
+
);
|
|
121
|
+
container.addChild(new Text(theme.fg("muted", ENGINE_PICKER_INTRO), 1, 0));
|
|
122
|
+
container.addChild(new Spacer(1));
|
|
123
|
+
|
|
124
|
+
const list = new SelectList(ENGINE_PICKER_ITEMS, ENGINE_PICKER_ITEMS.length, {
|
|
125
|
+
selectedPrefix: (t: string) => theme.fg("accent", t),
|
|
126
|
+
selectedText: (t: string) => theme.fg("accent", t),
|
|
127
|
+
description: (t: string) => theme.fg("muted", t),
|
|
128
|
+
scrollInfo: (t: string) => theme.fg("dim", t),
|
|
129
|
+
noMatch: (t: string) => theme.fg("warning", t),
|
|
130
|
+
});
|
|
131
|
+
list.onSelect = (item) => done(toEngine(item.value));
|
|
132
|
+
list.onCancel = () => done(null);
|
|
133
|
+
container.addChild(list);
|
|
134
|
+
|
|
135
|
+
container.addChild(new Spacer(1));
|
|
136
|
+
container.addChild(
|
|
137
|
+
new Text(theme.fg("dim", "↑↓ navigate · enter select · esc decide later"), 1, 0),
|
|
138
|
+
);
|
|
139
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
render: (width: number) => container.render(width),
|
|
143
|
+
invalidate: () => container.invalidate(),
|
|
144
|
+
handleInput: (data: string) => {
|
|
145
|
+
list.handleInput(data);
|
|
146
|
+
tui.requestRender();
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
overlay: true,
|
|
152
|
+
overlayOptions: { anchor: "center" as const, width: 80, maxHeight: "85%" as const, margin: 1 },
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
}
|