@estebanforge/pi-antigravity-bridge 1.8.0 → 1.8.2
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 +30 -0
- package/README.md +6 -6
- package/extensions/index.ts +66 -36
- package/package.json +2 -2
- package/src/acp/auth.ts +44 -6
- package/src/acp/connection.ts +11 -0
- package/src/acp/setup.ts +53 -6
- package/src/config.ts +36 -6
- package/src/engine-picker.ts +53 -67
- package/src/provider.ts +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,36 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.8.2] - 2026-10-13
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Released the dependency floor the 1.8.1 code already required.** 1.8.1 imports `dialogChoice` from `@estebanforge/pi-tui-dialogs` but still declared `^0.1.1` — legal semver, yet that version predates the export, so any consumer whose lockfile still held 0.1.1 loaded the new engine picker against the old library and `/agy engine` and the first-run picker died with `(0, _piTuiDialogs.dialogChoice) is not a function`. The floor is now `^0.1.2` (the first version carrying `dialogChoice`, already on npm), so updating the bridge lifts the library everywhere and a stale lock fails at install time instead of at runtime.
|
|
12
|
+
|
|
13
|
+
## [1.8.1] - 2026-10-13
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **The engine picker rides the shared dialog kit.** `showEnginePicker` now renders through `@estebanforge/pi-tui-dialogs`' new `dialogChoice` primitive (extracted from this picker) instead of a hand-rolled SelectList overlay: same title, intro, options, preselect, and esc-means-decide-later semantics, now drawn in the kit's framed style. Esc resolves `undefined` instead of `null`; both call sites are updated.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **An unrelated config save no longer promotes a legacy install to the acp default.** `saveConfig` merged `DEFAULTS` over the raw file, so any save (mode, digest, the patch-cleanup notice) on a config file predating the engine key wrote `engine: "acp"` to disk, defeating the legacy continuity rule on the next start. `saveConfig` now persists the resolved legacy engine (`stream-json`) for key-less files, honors explicit `patch.engine`, and never bakes `AGY_ENGINE` into the file.
|
|
22
|
+
|
|
23
|
+
- **The choice modal windows its option list.** `dialogChoice` computed its SelectList window at construction and handed the frame the full list, so on short terminals or with a long description the frame clipped trailing options while the selection could still move into the invisible region. The list viewport now follows the frame height per render (the settings menu's rule), with room for SelectList's `(n/m)` scroll-position line, and the tests pin a 10-option dialog on a 12-row terminal.
|
|
24
|
+
|
|
25
|
+
- **New installs default to the ACP engine; stream-json is the fallback.** `config.engine` unset (no file key, no `AGY_ENGINE`) now resolves to `acp` (`DEFAULT_ENGINE` in config.ts), and stale/garbage stored values fall back to that same default instead of stream-json. One carve-out for continuity: an existing config file that predates the engine key (no `engine` field at all) keeps `stream-json` — the acp default is for new installs, and an existing install must not silently start the ~1.5 GB binary download. The first-run picker preselects acp and explains the trade in that order; the existing startup self-heal covers the rest (binary install + sign-in chain on engine acp). Both engines stay fully supported and every existing config with a saved engine is untouched.
|
|
26
|
+
|
|
27
|
+
- **The `/agy` settings picker refuses plan against the on-disk engine too.** The plan-mode guard checked only the session-latched engine; since the Switch engine row can change the on-disk engine mid-session without a restart, it now blocks on either (latched or saved) so plan + ACP can never land on disk through the settings surface.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- **The `/agy` settings picker has a Switch engine row again** (first row, showing the active engine as its value). It is deliberately not a cycle row: Enter on it closes settings and opens the same picker modal `/agy engine` uses — explanations, plan-blocks-acp guard, and the acp pick's self-service setup + sign-in chain all come from the one shared path. Esc in the picker changes nothing.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **ACP sign-in no longer fails on servers that authenticate without writing `acp_token.json`.** Server 1.1.1 can answer `authenticate` OK from legacy `~/.gemini` credential artifacts (observed on darwin/arm64: instant OK, no browser, no token file, working turns) — but `/agy auth` treated the absent token file as a distinct failure and the session-start self-heal nagged login-pending forever on a fully working setup. The token file is a server-internal detail, not a contract. `/agy auth` now verifies the token-absent branch with a live `session/new` probe on the same connection: probe success is a real sign-in (result carries a note, the success toast says so), probe failure is an honest "no usable credentials" error that covers the lazily started browser flow. Both verified paths record a bridge-side marker (`~/.pi/agent/antigravity-bridge/acp-auth-verified.json`, a boolean plus timestamp, never a credential); `ensureAcpReady` treats the marker as login-done, `/agy doctor` shows `oauth-personal (verified, no token file)`, and any server-side `-32000` clears the marker so a stale one never masks a real logout.
|
|
36
|
+
|
|
7
37
|
## [1.8.0] - 2026-10-12
|
|
8
38
|
|
|
9
39
|
### Changed
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-antigravity-bridge
|
|
2
2
|
|
|
3
|
-
A Gemini model provider **and** the `AskAntigravity` delegation tool for [pi](https://github.com/earendil-works/pi), built on Google's official Antigravity binaries:
|
|
3
|
+
A Gemini model provider **and** the `AskAntigravity` delegation tool for [pi](https://github.com/earendil-works/pi), built on Google's official Antigravity binaries: **Google's official ACP server** by default (new installs), with the `agy` CLI engine kept as the fallback. It registers `antigravity/gemini-*` models in pi's `/model` picker (streaming), and provides the `AskAntigravity` tool for one-shot delegation - the same combined shape as `pi-claude-bridge`.
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
If you also have [`@estebanforge/pi-ask-antigravity`](https://github.com/EstebanForge/pi-extensions/tree/main/packages/pi-ask-antigravity) installed, this bridge takes over: pi-ask-antigravity detects the bridge and registers nothing, so the `AskAntigravity` tool is never duplicated.
|
|
@@ -13,12 +13,12 @@ Multi-turn works. The provider binds a pi session to an agy conversation id (per
|
|
|
13
13
|
|
|
14
14
|
### Two engines
|
|
15
15
|
|
|
16
|
-
Turns run through one of two engines behind the same provider surface (`config.engine`, default `
|
|
16
|
+
Turns run through one of two engines behind the same provider surface (`config.engine`, default `acp`):
|
|
17
17
|
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
18
|
+
- **acp** (default): Google's official ACP server (`agy_acp_server.par`), JSON-RPC 2.0 over stdio. 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 runs on live client-side estimates via `acp.usageEstimate`, default `estimate`, streamed per delta and superseded automatically the day the server starts sending real per-turn usage) and no cancel (abort tears the server down and reloads it next turn).
|
|
19
|
+
- **stream-json** (fallback): the persistent `agy` CLI process. Live token usage; conversation resume via `--conversation`.
|
|
20
20
|
|
|
21
|
-
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 (
|
|
21
|
+
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 (acp 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. Fresh installs default to acp and the startup self-heal installs the binary + chains the sign-in; with stream-json active and the `agy` binary missing, pi warns on every start until the binary is found.
|
|
22
22
|
|
|
23
23
|
Full capability comparison, switching, and setup/auth details: [docs/ENGINES.md](docs/ENGINES.md).
|
|
24
24
|
|
|
@@ -147,7 +147,7 @@ The `activate_skill` catalog mirrors pi's directory-based skill discovery: the t
|
|
|
147
147
|
/agy tools [hide|show <name>|reset] session-only bridge-catalog hides (multi-word names OK; reset restores the full set; bare lists exposed + hidden; never touches saved config)
|
|
148
148
|
/agy web on|off register the agy_web_search + agy_read_url pi tools for ANY provider's model (default off; applies on the next pi start or /reload; every call spawns agy and spends Antigravity quota)
|
|
149
149
|
/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)
|
|
150
|
-
/agy engine acp|stream-json switch the turn engine (restart to apply; default stream-json
|
|
150
|
+
/agy engine acp|stream-json switch the turn engine (restart to apply; default acp, stream-json is the fallback; acp runs self-service setup: binary install + auth bootstrap). No arguments opens the engine picker modal (TUI)
|
|
151
151
|
/agy auth-manual manual ACP credential setup (fallback; auto-setup normally covers this; default login = your Antigravity subscription, same account as the agy CLI)
|
|
152
152
|
/agy clear drop all session bindings (force fresh conversations)
|
|
153
153
|
```
|
package/extensions/index.ts
CHANGED
|
@@ -58,11 +58,12 @@ import { hookScriptSource, removeGateHooks, stageGateHooks, sweepWorkspaceGateGr
|
|
|
58
58
|
import { StreamDriver } from "../src/driver.js";
|
|
59
59
|
import { AcpDriver } from "../src/acp/driver.js";
|
|
60
60
|
import { runAcpAuth } from "../src/acp/auth.js";
|
|
61
|
+
import { setAuthInvalidatedHook } from "../src/acp/connection.js";
|
|
61
62
|
import { setupAuthUrlCapture } from "../src/acp/browser-capture.js";
|
|
62
|
-
import { ensureAcpReady, inspectAcpSetup } from "../src/acp/setup.js";
|
|
63
|
+
import { clearAcpAuthVerified, ensureAcpReady, inspectAcpSetup } from "../src/acp/setup.js";
|
|
63
64
|
import type { TurnDriver, TurnOutcome } from "../src/driver-types.js";
|
|
64
65
|
import { CONFIG_PATH, loadConfig, logsDir, MAX_TURN_CAP_MIN, parseCapMinutes, saveConfig, type AgyMode, type BridgeTools, type Engine, type ThinkingTier } from "../src/config.js";
|
|
65
|
-
import { agyMissingMessage, isAgyInstalled, savedEngineMessage, showEnginePicker, shouldOfferEnginePicker } from "../src/engine-picker.js";
|
|
66
|
+
import { agyMissingMessage, ENGINE_SETTINGS_ROW_ID, engineSettingsRow, isAgyInstalled, savedEngineMessage, showEnginePicker, shouldOfferEnginePicker } from "../src/engine-picker.js";
|
|
66
67
|
import { checkAgyCliVersion, describeAgyVersionCheck, MIN_AGY_VERSION } from "../src/agy-version.js";
|
|
67
68
|
import { isValidAgyAgentName, listAgyAgents } from "../src/agents.js";
|
|
68
69
|
import { formatSubagentRoster, SubagentRoster } from "../src/subagent-roster.js";
|
|
@@ -98,6 +99,11 @@ import { Type } from "typebox";
|
|
|
98
99
|
import { patchStatus, restorePatch } from "../src/patch-cleanup.js";
|
|
99
100
|
import { withDialogLock } from "../src/dialog-lock.js";
|
|
100
101
|
|
|
102
|
+
// The server disowning our verified marker (-32000 anywhere: turn, session
|
|
103
|
+
// new/load, cancel) must drop it, or ensureAcpReady keeps reporting a login
|
|
104
|
+
// that no longer exists. Wire once at load; the hook is a no-op until set.
|
|
105
|
+
setAuthInvalidatedHook(() => clearAcpAuthVerified());
|
|
106
|
+
|
|
101
107
|
// Last UI seen (session_start / /agy commands). The ACP login URL arrives
|
|
102
108
|
// via the driver log sink, which has no command context; the stash lets that
|
|
103
109
|
// sink toast instead of only logging to stderr. Module scope: both the
|
|
@@ -217,9 +223,9 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
217
223
|
// login URLs would silently stop appearing on headless boxes.
|
|
218
224
|
console.error("[antigravity-bridge] OAuth URL capture unavailable; login URL surfacing is off (setup failed).");
|
|
219
225
|
}
|
|
220
|
-
// Two turn engines behind one contract (plan §9):
|
|
221
|
-
// default) and the
|
|
222
|
-
//
|
|
226
|
+
// Two turn engines behind one contract (plan §9): the official ACP
|
|
227
|
+
// server (default for new installs) and the stream-json `agy` CLI
|
|
228
|
+
// (fallback). Neither spawns anything until its first turn.
|
|
223
229
|
// ACP log routing: only genuine failures reach stderr. Routine lifecycle
|
|
224
230
|
// (driver-created, spawn, session-new, ...) stays in the driver's
|
|
225
231
|
// #lifecycle ring buffer, visible via /agy doctor. An unfiltered sink fired
|
|
@@ -605,8 +611,12 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
605
611
|
log: acpLog,
|
|
606
612
|
});
|
|
607
613
|
fileLog.log("acp-auth", r.ok ? { ok: true } : { ok: false, error: r.error }, r.ok ? "info" : "warn");
|
|
608
|
-
if (r.ok)
|
|
609
|
-
|
|
614
|
+
if (r.ok) {
|
|
615
|
+
ctx.ui.notify(
|
|
616
|
+
r.note ? `Signed in (${r.note}). The ACP engine is ready; restart applies it.` : "Signed in. The ACP engine is ready; restart applies it.",
|
|
617
|
+
"info",
|
|
618
|
+
);
|
|
619
|
+
} else ctx.ui.notify(`ACP sign-in failed (${r.error}).\nRun /agy auth to retry; /agy auth-manual has manual steps.`, "warning");
|
|
610
620
|
} catch (err) {
|
|
611
621
|
ctx.ui.setStatus("agy-acp", undefined);
|
|
612
622
|
fileLog.log("acp-setup", { error: String(err) }, "warn");
|
|
@@ -1102,7 +1112,7 @@ function statusText(ctx: AgyCommandCtx): string {
|
|
|
1102
1112
|
const source = ctx.usingFallback ? "fallback (agy models failed)" : "discovered";
|
|
1103
1113
|
return [
|
|
1104
1114
|
"Antigravity bridge",
|
|
1105
|
-
` engine: ${config.engine}${config.engine === "
|
|
1115
|
+
` engine: ${config.engine}${config.engine === "stream-json" ? " (agy CLI fallback)" : " (official server)"}`,
|
|
1106
1116
|
` models: ${ctx.entries.length} ${source}`,
|
|
1107
1117
|
...settingsRows(ctx),
|
|
1108
1118
|
` sessions: ${ctx.store.size} bound`,
|
|
@@ -1222,23 +1232,7 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
1222
1232
|
// the explanations, not a bare usage line. Semantics match the
|
|
1223
1233
|
// direct path above: plan blocks acp, an acp pick chains setup
|
|
1224
1234
|
// + sign-in immediately, restart applies the switch.
|
|
1225
|
-
|
|
1226
|
-
const picked = await showEnginePicker(ui);
|
|
1227
|
-
if (picked === null) {
|
|
1228
|
-
ui.notify(`engine unchanged: ${current}.`, "info");
|
|
1229
|
-
return;
|
|
1230
|
-
}
|
|
1231
|
-
if (picked === current) {
|
|
1232
|
-
ui.notify(`engine is already ${current}. Restart applies it if set this session.`, "info");
|
|
1233
|
-
return;
|
|
1234
|
-
}
|
|
1235
|
-
if (picked === "acp" && loadConfig().mode === "plan") {
|
|
1236
|
-
ui.notify("mode is plan; the ACP engine has no plan mode. /agy mode accept-edits first.", "warning");
|
|
1237
|
-
return;
|
|
1238
|
-
}
|
|
1239
|
-
saveConfig({ engine: picked });
|
|
1240
|
-
ui.notify(savedEngineMessage(picked), "info");
|
|
1241
|
-
if (picked === "acp") void ctx.runAcpPickSetup({ ui });
|
|
1235
|
+
await runEngineModal(ui, ctx);
|
|
1242
1236
|
} else {
|
|
1243
1237
|
ui?.notify(`current engine: ${loadConfig().engine}\nusage: /agy engine stream-json|acp`, "info");
|
|
1244
1238
|
}
|
|
@@ -1282,7 +1276,10 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
1282
1276
|
});
|
|
1283
1277
|
ctx.fileLog.log("acp-auth", r.ok ? { ok: true } : { ok: false, error: r.error }, r.ok ? "info" : "warn");
|
|
1284
1278
|
if (r.ok) {
|
|
1285
|
-
ui?.notify(
|
|
1279
|
+
ui?.notify(
|
|
1280
|
+
r.note ? `Signed in (${r.note}). The ACP engine is ready; takes effect on the next pi start (or /reload).` : "Signed in. The ACP engine is ready; takes effect on the next pi start (or /reload).",
|
|
1281
|
+
"info",
|
|
1282
|
+
);
|
|
1286
1283
|
} else {
|
|
1287
1284
|
ui?.notify(`ACP sign-in failed (${r.error}).\nRun /agy auth to retry; /agy auth-manual has manual steps.`, "warning");
|
|
1288
1285
|
}
|
|
@@ -1794,14 +1791,17 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
1794
1791
|
}
|
|
1795
1792
|
|
|
1796
1793
|
/** Interactive settings picker (TUI only). Rows: the runtime config surface
|
|
1797
|
-
* (mode, permissions, model, thinking, bridge, digest, system prompt)
|
|
1798
|
-
*
|
|
1799
|
-
*
|
|
1794
|
+
* (mode, permissions, model, thinking, bridge, digest, system prompt) plus
|
|
1795
|
+
* the engine row, which resolves the menu on Enter and opens the same
|
|
1796
|
+
* picker modal as /agy engine (self-service binary + auth setup for acp). */
|
|
1800
1797
|
async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promise<void> {
|
|
1801
1798
|
const config = loadConfig();
|
|
1802
1799
|
const pending: PendingConfig = {};
|
|
1803
1800
|
|
|
1804
1801
|
const rows: DialogRow[] = [
|
|
1802
|
+
// Value-less action row: DialogMenu resolves the menu with this id on
|
|
1803
|
+
// Enter (see runEngineModal below); every other row cycles in place.
|
|
1804
|
+
engineSettingsRow(config.engine),
|
|
1805
1805
|
{
|
|
1806
1806
|
id: "mode",
|
|
1807
1807
|
label: "Execution mode",
|
|
@@ -1892,7 +1892,7 @@ async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promis
|
|
|
1892
1892
|
},
|
|
1893
1893
|
];
|
|
1894
1894
|
|
|
1895
|
-
await ui.custom<
|
|
1895
|
+
const resolved = await ui.custom<string | undefined>(
|
|
1896
1896
|
(host, theme, _kb, done) => {
|
|
1897
1897
|
const menu = new DialogMenu(
|
|
1898
1898
|
host,
|
|
@@ -1928,7 +1928,7 @@ async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promis
|
|
|
1928
1928
|
}
|
|
1929
1929
|
},
|
|
1930
1930
|
},
|
|
1931
|
-
() => done(),
|
|
1931
|
+
(rowId) => done(rowId),
|
|
1932
1932
|
);
|
|
1933
1933
|
return {
|
|
1934
1934
|
render: (w: number) => menu.render(w),
|
|
@@ -1942,14 +1942,18 @@ async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promis
|
|
|
1942
1942
|
DIALOG_OPTIONS,
|
|
1943
1943
|
);
|
|
1944
1944
|
|
|
1945
|
-
if (Object.keys(pending).length === 0)
|
|
1945
|
+
if (Object.keys(pending).length === 0) {
|
|
1946
|
+
if (resolved === ENGINE_SETTINGS_ROW_ID) await runEngineModal(ui, ctx);
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1946
1949
|
|
|
1947
1950
|
// The picker cannot switch engines (command-only: /agy engine), but the
|
|
1948
|
-
// mode row can still produce plan while the
|
|
1949
|
-
//
|
|
1950
|
-
//
|
|
1951
|
+
// mode row can still produce plan while the engine is acp - either the
|
|
1952
|
+
// session-latched engine or the on-disk one, which the Switch engine row
|
|
1953
|
+
// can change mid-session without a restart. Plan cannot be enforced on
|
|
1954
|
+
// provider turns on either engine (#1181 + RC01); refuse the combination.
|
|
1951
1955
|
const nextMode = pending.mode ?? config.mode;
|
|
1952
|
-
if (nextMode === "plan" && ctx.engine === "acp") {
|
|
1956
|
+
if (nextMode === "plan" && (ctx.engine === "acp" || config.engine === "acp")) {
|
|
1953
1957
|
ui.notify(
|
|
1954
1958
|
"plan cannot be enforced on provider turns on either engine (google-antigravity/antigravity-cli#1181; ACP additionally has no plan mode, RC01). /agy mode accept-edits.",
|
|
1955
1959
|
"warning",
|
|
@@ -1983,4 +1987,30 @@ async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promis
|
|
|
1983
1987
|
"error",
|
|
1984
1988
|
);
|
|
1985
1989
|
}
|
|
1990
|
+
|
|
1991
|
+
if (resolved === ENGINE_SETTINGS_ROW_ID) await runEngineModal(ui, ctx);
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
/** The engine-pick modal shared by /agy engine (no args) and the settings
|
|
1995
|
+
* picker's engine row: the same explanations as the first-run wizard and the
|
|
1996
|
+
* same semantics — plan blocks acp, an acp pick chains setup + sign-in
|
|
1997
|
+
* immediately, restart applies the switch. */
|
|
1998
|
+
async function runEngineModal(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promise<void> {
|
|
1999
|
+
const current = loadConfig().engine;
|
|
2000
|
+
const picked = await showEnginePicker(ui);
|
|
2001
|
+
if (picked === undefined) {
|
|
2002
|
+
ui.notify(`engine unchanged: ${current}.`, "info");
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2005
|
+
if (picked === current) {
|
|
2006
|
+
ui.notify(`engine is already ${current}. Restart applies it if set this session.`, "info");
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
if (picked === "acp" && loadConfig().mode === "plan") {
|
|
2010
|
+
ui.notify("mode is plan; the ACP engine has no plan mode. /agy mode accept-edits first.", "warning");
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
2013
|
+
saveConfig({ engine: picked });
|
|
2014
|
+
ui.notify(savedEngineMessage(picked), "info");
|
|
2015
|
+
if (picked === "acp") void ctx.runAcpPickSetup({ ui });
|
|
1986
2016
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@estebanforge/pi-antigravity-bridge",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.2",
|
|
4
4
|
"description": "Gemini provider for Pi on the Antigravity ACP server (official Google ACP) or the stream-json agy CLI. Use antigravity/* models in Pi. Full /model picker support. Models runs Pi's tools. ToS safe to use.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@estebanforge/pi-ask-shared": "^0.1.2",
|
|
58
|
-
"@estebanforge/pi-tui-dialogs": "^0.1.
|
|
58
|
+
"@estebanforge/pi-tui-dialogs": "^0.1.2",
|
|
59
59
|
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
60
60
|
}
|
|
61
61
|
}
|
package/src/acp/auth.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AcpConnection } from "./connection.js";
|
|
2
|
-
import { hasAcpToken } from "./setup.js";
|
|
2
|
+
import { hasAcpToken, writeAcpAuthVerified } from "./setup.js";
|
|
3
3
|
|
|
4
4
|
// Explicit sign-in (/agy auth): spawn a short-lived server, send
|
|
5
5
|
// `authenticate` (the RPC that fires the loopback listener + browser OAuth
|
|
@@ -9,6 +9,13 @@ import { hasAcpToken } from "./setup.js";
|
|
|
9
9
|
// blocks until the browser round-trip completes; the observed window is
|
|
10
10
|
// minutes-scale (one flow timed out at ~8.5 minutes), so the timeout is
|
|
11
11
|
// generous.
|
|
12
|
+
//
|
|
13
|
+
// Token-absent after an OK reply is NOT always a failure: server 1.1.1 can
|
|
14
|
+
// authenticate from legacy ~/.gemini credential artifacts and never writes
|
|
15
|
+
// acp_token.json (observed on darwin/arm64 2026-10-13). A live session/new
|
|
16
|
+
// probe separates that from a hollow accept: probe success = signed in
|
|
17
|
+
// (recorded in the bridge-side verified marker), probe failure = the server
|
|
18
|
+
// holds no usable credentials.
|
|
12
19
|
|
|
13
20
|
export const AUTH_TIMEOUT_MS = 10 * 60_000;
|
|
14
21
|
|
|
@@ -25,18 +32,41 @@ export interface AcpAuthOptions {
|
|
|
25
32
|
* authenticate replies, but write-after-reply is unverified; after a
|
|
26
33
|
* successful reply the run polls for the token up to this long before
|
|
27
34
|
* SIGTERM, so a laggard write is never killed mid-flight. A token that
|
|
28
|
-
* never appears is a
|
|
35
|
+
* never appears is verified through a live session probe instead of
|
|
36
|
+
* failing outright. */
|
|
29
37
|
tokenGraceMs?: number;
|
|
38
|
+
/** Bridge-side verified-sign-in marker. Written on every verified
|
|
39
|
+
* success (token file or live session probe). Default is the real
|
|
40
|
+
* bridge path; tests point this at a fixture. */
|
|
41
|
+
authVerifiedFile?: string;
|
|
30
42
|
timeoutMs?: number;
|
|
31
43
|
log?: (msg: string, data?: unknown) => void;
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
export interface AcpAuthResult {
|
|
35
47
|
ok: boolean;
|
|
48
|
+
/** Present when verified through the session probe: the server
|
|
49
|
+
* authenticated from existing credentials without writing a local
|
|
50
|
+
* token file. Absent on the normal token-file path and on failure. */
|
|
51
|
+
note?: string;
|
|
36
52
|
/** Failure reason (spawn, timeout, -32000 family, concurrent run). Absent when ok. */
|
|
37
53
|
error?: string;
|
|
38
54
|
}
|
|
39
55
|
|
|
56
|
+
/** Live credential probe for the token-absent branch: a server with usable
|
|
57
|
+
* credentials answers session/new; a hollow accept starts (or wants) a
|
|
58
|
+
* browser flow and the request times out or errors. Closes the probe
|
|
59
|
+
* session; the caller kills the connection either way. */
|
|
60
|
+
async function probeUsableCredentials(conn: AcpConnection, cwd?: string): Promise<boolean> {
|
|
61
|
+
try {
|
|
62
|
+
const s = await conn.newSession(cwd ?? process.cwd());
|
|
63
|
+
await conn.closeSession(s.sessionId).catch(() => {});
|
|
64
|
+
return true;
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
40
70
|
let authInFlight: Promise<AcpAuthResult> | null = null;
|
|
41
71
|
|
|
42
72
|
export function runAcpAuth(opts: AcpAuthOptions): Promise<AcpAuthResult> {
|
|
@@ -59,11 +89,19 @@ export function runAcpAuth(opts: AcpAuthOptions): Promise<AcpAuthResult> {
|
|
|
59
89
|
const deadline = Date.now() + grace;
|
|
60
90
|
while (!hasAcpToken(opts.tokenDir) && Date.now() < deadline) await sleep(200);
|
|
61
91
|
if (!hasAcpToken(opts.tokenDir)) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
92
|
+
// authenticate replied but no token file: either the server
|
|
93
|
+
// authenticated from artifacts outside antigravity-acp, or the
|
|
94
|
+
// reply is hollow. A live session/new separates the two.
|
|
95
|
+
if (!(await probeUsableCredentials(conn, opts.cwd))) {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
error: `the server accepted the sign-in but holds no usable credentials (session probe failed; token file absent in ${opts.tokenDir ?? "~/.gemini/antigravity-acp"}). Finish the Google sign-in if a browser opened, then retry /agy auth.`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
writeAcpAuthVerified(opts.authVerifiedFile);
|
|
102
|
+
return { ok: true, note: "the server authenticated from existing credentials; no browser sign-in was needed" };
|
|
66
103
|
}
|
|
104
|
+
writeAcpAuthVerified(opts.authVerifiedFile);
|
|
67
105
|
return { ok: true };
|
|
68
106
|
} catch (err) {
|
|
69
107
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
package/src/acp/connection.ts
CHANGED
|
@@ -50,6 +50,16 @@ export class AcpAuthError extends Error {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/** Fired exactly when the SERVER reports an auth failure (-32000 family).
|
|
54
|
+
* The extension entry wires this to clearAcpAuthVerified(): a verified
|
|
55
|
+
* marker must never outlive the server disagreeing with it. Kept as a hook
|
|
56
|
+
* so the low-level connection stays decoupled from bridge file paths. */
|
|
57
|
+
let authInvalidatedHook: (() => void) | null = null;
|
|
58
|
+
|
|
59
|
+
export function setAuthInvalidatedHook(fn: (() => void) | null): void {
|
|
60
|
+
authInvalidatedHook = fn;
|
|
61
|
+
}
|
|
62
|
+
|
|
53
63
|
export interface AcpConnectionOptions {
|
|
54
64
|
/** Process working directory for the spawned server. */
|
|
55
65
|
cwd: string;
|
|
@@ -613,6 +623,7 @@ export function translateError(method: string, err: unknown): Error {
|
|
|
613
623
|
if (err instanceof AcpAuthError) return err;
|
|
614
624
|
if (err instanceof JsonRpcResponseError) {
|
|
615
625
|
if (err.code === -32000) {
|
|
626
|
+
authInvalidatedHook?.();
|
|
616
627
|
const detail = typeof err.data === "object" && err.data !== null ? (err.data as { message?: unknown }).message : undefined;
|
|
617
628
|
const detailText = typeof detail === "string" ? `: ${detail}` : `: ${err.message}`;
|
|
618
629
|
return new AcpAuthError(`ACP authentication required for ${method}${detailText}. Run /agy auth to sign in; /agy auth-manual has manual steps.`);
|
package/src/acp/setup.ts
CHANGED
|
@@ -29,6 +29,10 @@ export interface SetupOptions {
|
|
|
29
29
|
installRoot?: string;
|
|
30
30
|
/** Server settings dir. Default ~/.gemini/antigravity-acp. */
|
|
31
31
|
geminiDir?: string;
|
|
32
|
+
/** Bridge-side verified-sign-in marker file. Default
|
|
33
|
+
* ~/.pi/agent/antigravity-bridge/acp-auth-verified.json (tests point this
|
|
34
|
+
* at a fixture to stay hermetic). */
|
|
35
|
+
authVerifiedFile?: string;
|
|
32
36
|
/** acp.bin from config (second resolution priority after AGY_ACP_BIN). */
|
|
33
37
|
configBin?: string;
|
|
34
38
|
/** Environment. Default process.env (test injection). */
|
|
@@ -244,6 +248,41 @@ export function hasAcpToken(dir?: string): boolean {
|
|
|
244
248
|
return tokenPresent(dir ?? path.join(os.homedir(), ".gemini", "antigravity-acp"));
|
|
245
249
|
}
|
|
246
250
|
|
|
251
|
+
/** Bridge-side record that the ACP server proved its own credentials in a
|
|
252
|
+
* verified sign-in (runAcpAuth). Needed because servers may authenticate
|
|
253
|
+
* from artifacts this module cannot see: 1.1.1 honors legacy ~/.gemini
|
|
254
|
+
* credential files (e.g. jetski-standalone-oauth-token) and never writes
|
|
255
|
+
* acp_token.json, so the file check alone would nag login-pending forever
|
|
256
|
+
* on a working setup. Cleared the moment the server reports an auth error
|
|
257
|
+
* (-32000), so a stale marker never masks a real logout. Never holds a
|
|
258
|
+
* credential value: a boolean and a timestamp. */
|
|
259
|
+
export function defaultAcpAuthVerifiedPath(): string {
|
|
260
|
+
return path.join(os.homedir(), ".pi", "agent", "antigravity-bridge", "acp-auth-verified.json");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function readAcpAuthVerified(file: string = defaultAcpAuthVerifiedPath()): boolean {
|
|
264
|
+
try {
|
|
265
|
+
const raw = JSON.parse(fs.readFileSync(file, "utf8")) as { verified?: unknown };
|
|
266
|
+
return raw.verified === true;
|
|
267
|
+
} catch {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function writeAcpAuthVerified(file: string = defaultAcpAuthVerifiedPath()): void {
|
|
273
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
274
|
+
fs.writeFileSync(file, `${JSON.stringify({ verified: true, at: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function clearAcpAuthVerified(file: string = defaultAcpAuthVerifiedPath()): void {
|
|
278
|
+
try {
|
|
279
|
+
fs.rmSync(file, { force: true });
|
|
280
|
+
} catch {
|
|
281
|
+
/* best effort: a leftover marker only over-reports readiness, and the
|
|
282
|
+
next auth error clears it again. */
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
247
286
|
/** Auth state WITHOUT reading any credential value: settings.json carries
|
|
248
287
|
* only auth.type (+ non-secret gcp placement), acp_token.json is stat()ed. */
|
|
249
288
|
export function readAuthState(dir?: string): AuthState {
|
|
@@ -302,8 +341,14 @@ export function resolveInstalledBinary(opts: SetupOptions = {}): BinaryResolutio
|
|
|
302
341
|
/** Read-only setup state (for /agy doctor): no installs, no writes. */
|
|
303
342
|
export function inspectAcpSetup(opts: SetupOptions = {}): { bin: string | null; source: string | null; auth: string | null } {
|
|
304
343
|
const binary = resolveInstalledBinary(opts);
|
|
305
|
-
const
|
|
306
|
-
|
|
344
|
+
const gdir = defaultGeminiDir(opts);
|
|
345
|
+
const auth = readAuthState(gdir);
|
|
346
|
+
if (!auth.configured) return { bin: binary.bin, source: binary.source, auth: null };
|
|
347
|
+
const type = auth.type ?? "token";
|
|
348
|
+
// Surface the verified-marker state so a setup that authenticates without
|
|
349
|
+
// a token file (1.1.1 legacy artifacts) reads as healthy in doctor.
|
|
350
|
+
const verified = type !== "gemini-api-key" && !tokenPresent(gdir) && readAcpAuthVerified(opts.authVerifiedFile);
|
|
351
|
+
return { bin: binary.bin, source: binary.source, auth: verified ? `${type} (verified, no token file)` : type };
|
|
307
352
|
}
|
|
308
353
|
|
|
309
354
|
/** Orchestrate everything the ACP engine needs, in order: binary (install
|
|
@@ -358,10 +403,12 @@ export async function ensureAcpReady(opts: SetupOptions = {}): Promise<AcpSetupS
|
|
|
358
403
|
}
|
|
359
404
|
// settings.json alone proves nothing: setup writes it before the login,
|
|
360
405
|
// the token file only appears after the browser round-trip. oauth-personal
|
|
361
|
-
// without a token
|
|
362
|
-
//
|
|
363
|
-
// gemini
|
|
364
|
-
|
|
406
|
+
// without a token is login-pending UNLESS a verified sign-in recorded that
|
|
407
|
+
// this server authenticates from artifacts the bridge cannot see (1.1.1
|
|
408
|
+
// legacy ~/.gemini credentials; marker written by runAcpAuth, cleared on
|
|
409
|
+
// any -32000). gemini-api-key has no browser login; a present token file
|
|
410
|
+
// means done.
|
|
411
|
+
const needsLogin = auth.type !== "gemini-api-key" && !tokenPresent(gdir) && !readAcpAuthVerified(opts.authVerifiedFile);
|
|
365
412
|
return {
|
|
366
413
|
ok: true,
|
|
367
414
|
bin: binary.bin!,
|
package/src/config.ts
CHANGED
|
@@ -38,9 +38,16 @@ export function logsDir(): string {
|
|
|
38
38
|
);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
/** Which turn engine drives turns. "
|
|
42
|
-
*
|
|
41
|
+
/** Which turn engine drives turns. "acp" (the official server) is the
|
|
42
|
+
* default for new installs; "stream-json" (the `agy` CLI) remains a fully
|
|
43
|
+
* supported fallback. */
|
|
43
44
|
export type Engine = "stream-json" | "acp";
|
|
45
|
+
|
|
46
|
+
/** The engine a fresh install runs when nothing is configured (no file key,
|
|
47
|
+
* no AGY_ENGINE): the official ACP server. Unknown/stale stored values also
|
|
48
|
+
* fall back here; the first startup's acp self-heal installs the binary and
|
|
49
|
+
* chains the sign-in. */
|
|
50
|
+
export const DEFAULT_ENGINE: Engine = "acp";
|
|
44
51
|
export type AgyMode = "accept-edits" | "plan";
|
|
45
52
|
export type ThinkingTier = "low" | "medium" | "high";
|
|
46
53
|
export type BridgeTools = "none" | "mcp" | "all";
|
|
@@ -187,7 +194,7 @@ export interface AgyConfig {
|
|
|
187
194
|
}
|
|
188
195
|
|
|
189
196
|
const DEFAULTS: AgyConfig = {
|
|
190
|
-
engine:
|
|
197
|
+
engine: DEFAULT_ENGINE,
|
|
191
198
|
mode: "accept-edits",
|
|
192
199
|
readOnly: false,
|
|
193
200
|
skipPermissions: true,
|
|
@@ -250,9 +257,16 @@ export function loadConfig(configPath: string = CONFIG_PATH): AgyConfig {
|
|
|
250
257
|
// explicit AGY_MODE=accept-edits when the file says plan, violating the
|
|
251
258
|
// documented precedence. Check env first.
|
|
252
259
|
// Engine: narrow to the known set; anything else (incl. the pre-1.3.2
|
|
253
|
-
// "sqlite" value) falls back to the
|
|
254
|
-
|
|
255
|
-
|
|
260
|
+
// "sqlite" value) falls back to the default engine, same as an absent key.
|
|
261
|
+
// One absence exception: a config file WITHOUT an engine key belongs to an
|
|
262
|
+
// existing install (saveConfig persists the resolved legacy engine), and
|
|
263
|
+
// the acp default is for new installs only - an existing install keeps
|
|
264
|
+
// the legacy stream-json engine instead of silently starting the ~1.5 GB
|
|
265
|
+
// acp download. existsSync is false on EACCES too, which reads as fresh
|
|
266
|
+
// and picks acp; an unreadable config dir is a broken install either way.
|
|
267
|
+
const freshInstall = !fs.existsSync(configPath);
|
|
268
|
+
const engineFallback = freshInstall || file.engine !== undefined ? DEFAULT_ENGINE : "stream-json";
|
|
269
|
+
const engine = narrowEngine(String(process.env.AGY_ENGINE ?? file.engine ?? engineFallback).toLowerCase());
|
|
256
270
|
|
|
257
271
|
const mode: AgyMode =
|
|
258
272
|
process.env.AGY_MODE !== undefined
|
|
@@ -377,6 +391,14 @@ export function loadConfig(configPath: string = CONFIG_PATH): AgyConfig {
|
|
|
377
391
|
};
|
|
378
392
|
}
|
|
379
393
|
|
|
394
|
+
/** Narrow a raw engine string to the known set; anything unknown (incl. the
|
|
395
|
+
* pre-1.3.2 "sqlite" value) falls to the default engine. Shared by
|
|
396
|
+
* loadConfig's effective-engine resolution and saveConfig's persistence so
|
|
397
|
+
* the two can never drift. */
|
|
398
|
+
function narrowEngine(raw: string): Engine {
|
|
399
|
+
return raw === "acp" || raw === "stream-json" ? raw : DEFAULT_ENGINE;
|
|
400
|
+
}
|
|
401
|
+
|
|
380
402
|
/** Atomically persist a config patch (temp + rename). Merges onto the RAW
|
|
381
403
|
* FILE values, never loadConfig(): env overrides must not be baked into the
|
|
382
404
|
* file (AGY_READONLY=1 in the env would otherwise persist readOnly: true to
|
|
@@ -390,6 +412,13 @@ export function saveConfig(patch: Partial<AgyConfig>, configPath: string = CONFI
|
|
|
390
412
|
// triggers normalization (a null becomes the default shape, never null).
|
|
391
413
|
const fileAcp: Partial<AcpConfig> = file.acp && typeof file.acp === "object" ? file.acp : {};
|
|
392
414
|
const fileApprovals: Partial<GateConfig> = file.approvals && typeof file.approvals === "object" ? file.approvals : {};
|
|
415
|
+
// Engine persistence follows loadConfig's continuity rule minus env (env
|
|
416
|
+
// must never be baked into the file). A legacy file without an engine key
|
|
417
|
+
// persists "stream-json": the bare DEFAULTS spread below would otherwise
|
|
418
|
+
// write the acp default into an existing install's file on any unrelated
|
|
419
|
+
// save, arming the ~1.5 GB download on the next start.
|
|
420
|
+
const persistedEngine = patch.engine ?? file.engine
|
|
421
|
+
?? (fs.existsSync(configPath) ? "stream-json" : DEFAULT_ENGINE);
|
|
393
422
|
const next: AgyConfig = {
|
|
394
423
|
...DEFAULTS,
|
|
395
424
|
...file,
|
|
@@ -400,6 +429,7 @@ export function saveConfig(patch: Partial<AgyConfig>, configPath: string = CONFI
|
|
|
400
429
|
? { approvals: { gateMode: fileApprovals.gateMode ?? DEFAULTS.approvals.gateMode, mode: fileApprovals.mode ?? DEFAULTS.approvals.mode } }
|
|
401
430
|
: {}),
|
|
402
431
|
...patch,
|
|
432
|
+
engine: narrowEngine(String(persistedEngine).toLowerCase()),
|
|
403
433
|
};
|
|
404
434
|
const dir = path.dirname(configPath);
|
|
405
435
|
fs.mkdirSync(dir, { recursive: true });
|
package/src/engine-picker.ts
CHANGED
|
@@ -7,43 +7,37 @@
|
|
|
7
7
|
// Every start with the stream-json engine active also re-checks that the
|
|
8
8
|
// `agy` binary exists and warns until it does (re-auth is out of scope).
|
|
9
9
|
//
|
|
10
|
-
// UI:
|
|
11
|
-
//
|
|
10
|
+
// UI: the shared framed choice modal from @estebanforge/pi-tui-dialogs
|
|
11
|
+
// (dialogChoice): one decision statement, a closed option list, esc cancels.
|
|
12
12
|
|
|
13
13
|
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
Container,
|
|
17
|
-
type SelectItem,
|
|
18
|
-
SelectList,
|
|
19
|
-
Spacer,
|
|
20
|
-
Text,
|
|
21
|
-
} from "@earendil-works/pi-tui";
|
|
14
|
+
import { dialogChoice, type ChoiceOption, type DialogContext, type DialogRow } from "@estebanforge/pi-tui-dialogs";
|
|
22
15
|
import fs from "node:fs";
|
|
23
16
|
import path from "node:path";
|
|
24
|
-
import type
|
|
17
|
+
import { DEFAULT_ENGINE, type Engine } from "./config.js";
|
|
25
18
|
|
|
26
|
-
/** Picker order is the default answer order:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
value: "stream-json",
|
|
30
|
-
label: "Stream-JSON CLI",
|
|
31
|
-
description: "default, recommended",
|
|
32
|
-
},
|
|
19
|
+
/** Picker order is the default answer order: acp (the default engine)
|
|
20
|
+
* preselected first, stream-json as the fallback. */
|
|
21
|
+
export const ENGINE_PICKER_ITEMS: ChoiceOption[] = [
|
|
33
22
|
{
|
|
34
23
|
value: "acp",
|
|
35
24
|
label: "ACP server (official Google)",
|
|
36
|
-
description: "
|
|
25
|
+
description: "default · official Google server",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
value: "stream-json",
|
|
29
|
+
label: "Stream-JSON CLI",
|
|
30
|
+
description: "the `agy` CLI, fallback",
|
|
37
31
|
},
|
|
38
32
|
];
|
|
39
33
|
|
|
40
34
|
/** Intro paragraph above the list. Text wraps, so explanations stay here and
|
|
41
|
-
*
|
|
35
|
+
* option descriptions stay one-liners (the choice list truncates long lines). */
|
|
42
36
|
export const ENGINE_PICKER_INTRO = [
|
|
43
37
|
"Pick the engine that runs your Antigravity turns. Switch anytime with /agy engine (restart applies it).",
|
|
44
38
|
"",
|
|
45
|
-
"
|
|
46
|
-
"
|
|
39
|
+
"ACP (default): 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).",
|
|
40
|
+
"stream-json (fallback): the `agy` CLI you already installed and authenticated. Persistent process, streamed output.",
|
|
47
41
|
].join("\n");
|
|
48
42
|
|
|
49
43
|
/** True only for a genuine first interactive run: no saved config yet (any
|
|
@@ -64,9 +58,9 @@ export function shouldOfferEnginePicker(
|
|
|
64
58
|
|
|
65
59
|
/** Narrow a picker value to an Engine. Items are our own constants, but an
|
|
66
60
|
* unknown value must never reach config as a cast string: it falls back to
|
|
67
|
-
* the
|
|
61
|
+
* the default engine (acp), same rule as loadConfig's narrowing. */
|
|
68
62
|
export function toEngine(value: string): Engine {
|
|
69
|
-
return value === "acp"
|
|
63
|
+
return value === "acp" || value === "stream-json" ? value : DEFAULT_ENGINE;
|
|
70
64
|
}
|
|
71
65
|
|
|
72
66
|
/** Toast copy shown after the choice is saved. ACP names the promise that
|
|
@@ -78,6 +72,23 @@ export function savedEngineMessage(engine: Engine): string {
|
|
|
78
72
|
: "Engine saved: stream-json. Restart pi to apply.";
|
|
79
73
|
}
|
|
80
74
|
|
|
75
|
+
/** /agy settings row that opens the engine picker. Deliberately value-less:
|
|
76
|
+
* DialogMenu cycles rows that carry `values`, but resolves the menu with the
|
|
77
|
+
* row id on Enter for rows without — the caller then closes settings and
|
|
78
|
+
* opens the same modal as the first-run wizard. The active engine rides as
|
|
79
|
+
* the display value so the row reads like the rest of the surface. */
|
|
80
|
+
export const ENGINE_SETTINGS_ROW_ID = "engine";
|
|
81
|
+
|
|
82
|
+
export function engineSettingsRow(engine: Engine): DialogRow {
|
|
83
|
+
return {
|
|
84
|
+
id: ENGINE_SETTINGS_ROW_ID,
|
|
85
|
+
label: "Switch engine",
|
|
86
|
+
description:
|
|
87
|
+
"Open the engine picker (stream-json CLI vs the official ACP server). Enter opens it and closes settings; esc in the picker changes nothing. Restart applies a switch.",
|
|
88
|
+
currentValue: engine,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
81
92
|
/** True when the `agy` CLI binary can be found. A binRef with a path
|
|
82
93
|
* separator (AGY_BIN=/opt/agy/agy) must exist as a file; a bare name is
|
|
83
94
|
* searched on PATH. statSync cannot throw through the guards, but a race
|
|
@@ -108,48 +119,23 @@ export function agyMissingMessage(): string {
|
|
|
108
119
|
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
120
|
}
|
|
110
121
|
|
|
111
|
-
/**
|
|
112
|
-
* the
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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)));
|
|
122
|
+
/** The kit's DialogContext wants the whole extension context, but both picker
|
|
123
|
+
* call sites hold only the ui object and are TUI-gated (ctx.mode === "tui")
|
|
124
|
+
* before they run. Wrap instead of threading two different ctx shapes. The
|
|
125
|
+
* cast is the pi-custom-options vs unknown-options signature gap; custom and
|
|
126
|
+
* notify exist at runtime on ExtensionUIContext. */
|
|
127
|
+
function asDialogContext(ui: ExtensionUIContext): DialogContext {
|
|
128
|
+
return { hasUI: true, mode: "tui", ui: ui as unknown as DialogContext["ui"] };
|
|
129
|
+
}
|
|
140
130
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
overlay: true,
|
|
152
|
-
overlayOptions: { anchor: "center" as const, width: 80, maxHeight: "85%" as const, margin: 1 },
|
|
153
|
-
},
|
|
154
|
-
);
|
|
131
|
+
/** Render the choice modal via the shared dialog kit. Resolves with the
|
|
132
|
+
* chosen engine, or undefined when the user pressed esc (decide later -
|
|
133
|
+
* nothing is persisted). */
|
|
134
|
+
export async function showEnginePicker(ctx: ExtensionUIContext): Promise<Engine | undefined> {
|
|
135
|
+
const picked = await dialogChoice(asDialogContext(ctx), "Antigravity Bridge: choose your engine", {
|
|
136
|
+
description: ENGINE_PICKER_INTRO,
|
|
137
|
+
options: ENGINE_PICKER_ITEMS,
|
|
138
|
+
footer: "↑↓ navigate · enter select · esc decide later",
|
|
139
|
+
});
|
|
140
|
+
return picked === undefined ? undefined : toEngine(picked);
|
|
155
141
|
}
|
package/src/provider.ts
CHANGED
|
@@ -345,8 +345,9 @@ export interface StreamSimpleDeps {
|
|
|
345
345
|
* driver and bridge calls park as toolUse round-trips. Required with
|
|
346
346
|
* roundTrips. */
|
|
347
347
|
driver?: TurnDriver;
|
|
348
|
-
/** Official-server ACP engine
|
|
349
|
-
* absent the config switch falls back to
|
|
348
|
+
/** Official-server ACP engine (the default engine). Selected via
|
|
349
|
+
* config.engine = "acp"; when absent the config switch falls back to
|
|
350
|
+
* the stream driver. */
|
|
350
351
|
acpDriver?: TurnDriver;
|
|
351
352
|
roundTrips?: ToolRoundTrips;
|
|
352
353
|
/** Replay store for the display-only antigravity wrapper tool. Required
|